committee-rails
とRSpecを連携させてOpenAPIの定義と実装が一致しているかテストしてたんですが、その際に"Content-Type" request header must be set to "application/json".
というエラーが出てしまい、解決に時間がかかりました。
備忘録としてまとめます。
バージョン
- Ruby: 3.2.2
- Rails: 7.1.3.2
- rspec-rails: 6.1.2
- committee-rails: 0.8.0
記事の信頼性
- ぼくは独学で未経験から従業員300名以上の自社開発企業へ転職しました。
- 実務ではVue.jsとRailsを毎日書いています。
- 初心者や駆け出しエンジニアがつまづくポイントも身をもってよく理解しています。
問題
学習のためスキーマ駆動開発の流れでTODOアプリを作っていました。
現在のOpenAPI定義は次のとおりです。
openapi: 3.0.0
info:
title: APIv1
version: 1.0.0
description: APIv1 for todo-app rails
servers:
- url: http://127.0.0.1:3000/
paths:
/api/v1/todos:
get:
tags: ["todo"]
operationId: apiV1GetTodos
summary: 全てのTodoを取得する
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
todos:
type: array
items:
$ref: "#/components/schemas/Todo"
post:
tags: ["todo"]
operationId: apiV1PostTodos
summary: Todoを作成する
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: "積読を減らす"
description:
type: string
example: "技術書を1冊読み終える"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Todo"
"400":
description: Bad Request
"403":
description: Forbidden
"422":
description: Unprocessable Entity
content:
application/json:
schema:
type: object
properties:
error:
type: array
items:
type: string
components:
schemas:
Todo:
properties:
id:
type: integer
format: int64
example: 1
title:
type: string
example: "積読を減らす"
description:
type: string
example: "技術書を1冊読み終える"
done:
type: boolean
default: false
required:
- id
- title
- done
この定義を元にした実装は次のとおりです。
class Api::V1::TodosController < ApplicationController
def index
@todos = Todo.all.order(:id)
render json: { todos: @todos }
end
def create
@todo = Todo.new(todo_params)
if @todo.save
render json: { todo: @todo }
else
render json: { error: @todo.errors.full_messages }, status: :unprocessable_entity
end
end
private
def todo_params
params.require(:todo).permit(:title, :description)
end
end
最後にこの2つのAPIに対するテストコードもRSPecで書きました。
require 'rails_helper'
RSpec.describe 'Api::V1::Todos', type: :request do
describe 'GET /index' do
it 'HTTPステータスが成功になること' do
get '/api/v1/todos'
expect(response).to have_http_status(:success)
end
it 'Todoのリストを返すこと' do
todo = Todo.create(title: 'Test Todo', done: false)
get '/api/v1/todos'
expect(response.body).to include(todo.title)
end
it 'Todoのリストを正しい順番で返すこと' do
todo1 = Todo.create(title: 'Test Todo 1', done: false)
todo2 = Todo.create(title: 'Test Todo 2', done: false)
get '/api/v1/todos'
expect(response.body.index(todo1.title)).to be < response.body.index(todo2.title)
end
it 'リクエストスキーマに準拠していること' do
get '/api/v1/todos'
assert_request_schema_confirm
end
it 'レスポンスコード200でレスポンススキーマに準拠していること' do
get '/api/v1/todos'
assert_response_schema_confirm(200)
end
end
describe 'POST /create' do
it 'HTTPステータスが成功になること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
expect(response).to have_http_status(:success)
end
it '新しいTodoを作成すること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
expect(Todo.last.title).to eq('Test Todo')
end
it 'リクエストスキーマに準拠していること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
assert_request_schema_confirm
end
it 'レスポンスコード200でレスポンススキーマに準拠していること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }
assert_response_schema_confirm(200)
end
it 'レスポンスコード422でレスポンススキーマに準拠していること' do
post '/api/v1/todos', params: { todo: { title: '' } }
assert_response_schema_confirm(422)
end
end
end
しかしテストを実行すると次のようにCommittee::InvalidRequest
とエラーが出てしまいます。
todo-app % docker compose run --rm rails rspec spec/requests/api/v1/todos_spec.rb
[+] Running 1/0
✔ Container todo-app-db-1 Running 0.0s
Api::V1::Todos
GET /index
returns http success
returns a list of todos
returns a list of todos in the correct order
conforms to request schema
conforms to response schema with 200 response code
POST /create
returns http success
creates a new todo
conforms to request schema (FAILED - 1)
conforms to response schema with 200 response code
conforms to response schema with 422 response code
Failures:
1) Api::V1::Todos POST /create conforms to request schema
Failure/Error: assert_request_schema_confirm
Committee::InvalidRequest:
"Content-Type" request header must be set to "application/json".
# /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3/request_validator.rb:35:in `check_content_type'
# /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3/request_validator.rb:16:in `call'
# /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3.rb:61:in `request_schema_validation'
# /usr/local/bundle/gems/committee-5.1.0/lib/committee/schema_validator/open_api_3.rb:18:in `request_validate'
# /usr/local/bundle/gems/committee-5.1.0/lib/committee/test/methods.rb:17:in `assert_request_schema_confirm'
# ./spec/requests/api/v1/todos_spec.rb:51:in `block (3 levels) in <top (required)>'
Finished in 0.21618 seconds (files took 2.1 seconds to load)
10 examples, 1 failure
Failed examples:
rspec ./spec/requests/api/v1/todos_spec.rb:49 # Api::V1::Todos POST /create conforms to request schema
todo-app %
openapi.yaml
にはapplication/json
を指定しているのになぜだ?となり、理由がわからずハマってしまいました、、。
なおRSpecとcommittee-rails
の連携はspec/rails_helper.rb
にて次のように記述しています。
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
abort e.to_s.strip
end
RSpec.configure do |config|
config.fixture_paths = [
Rails.root.join('spec/fixtures')
]
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
# モジュール名 (FactoryBot)を省略するために記述
config.include FactoryBot::Syntax::Methods
# OpenAPIのスキーマチェックを行う
config.include Committee::Rails::Test::Methods
config.add_setting :committee_options
config.committee_options = {
schema_path: Rails.root.join('docs/openapi/openapi.yaml'),
# committee 5 から明示的な指定が推奨された
strict_reference_validation: true
}
end
解決方法
結論として、次のようにRSpecのテストコードを修正することでテストが成功するようになりました。
require 'rails_helper'
RSpec.describe 'Api::V1::Todos', type: :request do
let(:request_header) { { 'Content-Type' => 'application/json' } }
describe 'GET /index' do
it 'HTTPステータスが成功になること' do
get '/api/v1/todos'
expect(response).to have_http_status(:success)
end
it 'Todoのリストを返すこと' do
todo = Todo.create(title: 'Test Todo', done: false)
get '/api/v1/todos'
expect(response.body).to include(todo.title)
end
it 'Todoのリストを正しい順番で返すこと' do
todo1 = Todo.create(title: 'Test Todo 1', done: false)
todo2 = Todo.create(title: 'Test Todo 2', done: false)
get '/api/v1/todos'
expect(response.body.index(todo1.title)).to be < response.body.index(todo2.title)
end
it 'リクエストスキーマに準拠していること' do
get '/api/v1/todos'
assert_request_schema_confirm
end
it 'レスポンスコード200でレスポンススキーマに準拠していること' do
get '/api/v1/todos'
assert_response_schema_confirm(200)
end
end
describe 'POST /create' do
it 'HTTPステータスが成功になること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
expect(response).to have_http_status(:success)
end
it '新しいTodoを作成すること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
expect(Todo.last.title).to eq('Test Todo')
end
it 'リクエストスキーマに準拠していること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
assert_request_schema_confirm
end
it 'レスポンスコード200でレスポンススキーマに準拠していること' do
post '/api/v1/todos', params: { todo: { title: 'Test Todo' } }.to_json, headers: request_header
assert_response_schema_confirm(200)
end
it 'レスポンスコード422でレスポンススキーマに準拠していること' do
post '/api/v1/todos', params: { todo: { title: '' } }.to_json, headers: request_header
assert_response_schema_confirm(422)
end
end
end
追加したのは以下の2点です。
params
をto_json
でJSON形式に変換headers
オプションでリクエストヘッダーを指定
OpenAPI定義でapplication/json
と定義しているため、params
をJSON形式へ変換した上で、リクエストヘッダーでもapplication/json
を指定する必要がありました。
リクエストボディにデータを含めてパラメータをサーバへ渡したい場合は、このように書かないといけないようです。
おわりに
てっきりOpenAPI定義の書き方やcommittee-rails
とRSpecの設定の問題だと思っていたんですが、RSpecの書き方が問題だったんですね。
コメント