REST APIのテストコードにレスポンスボディの検証を追加することで、レスポンスボディに修正が加わったことを機械的にチェックできます。
今回はRSpecでREST APIのレスポンスボディを検証する方法について紹介します。
目次
検証で利用するサンプルコードについて
検証用のAPIは、APIモードで作成したRailsアプリケーション上でscaffold
を実行することで作成しました。
今回はEvent
というモデルのAPIを作成しました。
$ rails g scaffold event title:string
RSpecは以下のようになります。
spec/requests/events_spec.rb
require 'rails_helper'
describe 'Events', type: :request do
describe 'GET /events' do
let!(:event) { create :event }
before do
get '/events'
end
it '取得できること' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body).length).to eq(1)
end
end
describe 'GET /events/:id' do
let!(:event) { create :event }
before do
get "/events/#{event.id}"
end
it '取得できること' do
expect(response).to have_http_status 200
end
end
describe 'POST /events' do
before do
post "/events", params: { event: { title: 'サンプルイベント' } }
end
it '登録できること' do
expect(response).to have_http_status 201
expect(Event.all.count).to eq 1
end
end
describe 'PATCH/PUT /events/:id' do
let!(:event) { create :event }
before do
patch "/events/#{event.id}", params: { event: { title: 'サンプルイベント更新' } }
end
it '更新できること' do
expect(response).to have_http_status 200
end
end
describe 'DELEET /events/:id' do
let!(:event) { create :event }
before do
delete "/events/#{event.id}"
end
it '削除できること' do
expect(response).to have_http_status 204
expect(Event.all.count).to eq 0
end
end
end
下準備: RSpec実行環境の構築
今回はテスティングフレームワークにRSpecを利用します。
また、テストデータを作成するためにFactoryBotを導入します。
実際に手元でRSpecの挙動を確認したい場合は以下のセットアップを行ってください。
Gemfile
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
end
$ bundle install
$ rails generate rspec:install
spec/rails_helper.rb
RSpec.configure do |config|
# 以下を追加
# - FactoryBotのシンタックスを省略できるようにする
# - 例. FactoryBot.build(:user) → build(:user)
config.include FactoryBot::Syntax::Methods
end
spec/factories/events.rb
FactoryBot.define do
factory :event do
title { "サンプルイベント" }
end
end
レスポンスボディの検証方法
FactoryBotで作成したテストデータの値をexpected
、APIを経由して取得したデータの値をactual
とし、それぞれをmatch
で検証します。
イベント詳細API(GET /events/:id
)のRSpecは以下のようになります。
spec/requests/events_spec.rb
describe 'GET /events/:id' do
let!(:event) { create :event }
let(:expected_response_object) do
{
'id' => "#{event.id}".to_i,
'title' => "#{event.title}",
'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
}
end
before do
get "/events/#{event.id}"
end
it '取得できること' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body)).to match(expected_response_object)
end
end
レスポンスボディの値は型だけチェックできれば問題ないという場合はa_kind_of
を利用します。
イベント一覧API(GET /events
)のRSpecは以下のようになります。
なお、イベント一覧APIは配列でデータが返されるのでall(match(...))
で各オブジェクトのプロパティをチェックしています。
spec/requests/events_spec.rb
describe 'GET /events' do
let!(:event) { create :event }
let(:json) { JSON.parse(response.body) }
let(:expected_response_properties) do
{
'id' => a_kind_of(Integer),
'title' => a_kind_of(String),
'created_at' => a_kind_of(String),
'updated_at' => a_kind_of(String),
}
end
before do
get '/events'
end
it '取得できること' do
expect(response).to have_http_status 200
expect(json.length).to eq(1)
expect(json).to all(match(expected_response_properties))
end
end
プロパティが多すぎて全て記載するのが大変な場合や、プロパティの一部だけ確認できれば問題ないという場合はmatch
の代わりにinclude
を利用します。
include
を利用したRSpecは以下の通りです。
spec/requests/events_spec.rb
describe 'GET /events/:id' do
let!(:event) { create :event }
let(:expected_response_object) do
{
'title' => "#{event.title}"
}
end
before do
get "/events/#{event.id}"
end
it '取得できること' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body)).to include(expected_response_object)
end
end
参考: レスポンスボディの検証を追加したCRUD APIのRSpec全体像
冒頭で紹介したRSpecに、レスポンスボディの検証を追加したものが以下になります。
spec/requests/events_spec.rb
require 'rails_helper'
describe 'Events', type: :request do
let(:json) { JSON.parse(response.body) }
describe 'GET /events' do
let!(:event) { create :event }
let(:expected_response_object) do
{
'id' => "#{event.id}".to_i,
'title' => "#{event.title}",
'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
}
end
let(:expected_response_properties) do
{
'id' => a_kind_of(Integer),
'title' => a_kind_of(String),
'created_at' => a_kind_of(String),
'updated_at' => a_kind_of(String),
}
end
before do
get '/events'
end
it '取得できること' do
expect(response).to have_http_status 200 # ステータスコードの検証
expect(json[0]).to match(expected_response_object) # レスポンスデータの検証
expect(json).to all(match(expected_response_properties)) # レスポンスのプロパティの検証
expect(json.length).to eq(1) # オブジェクト数の検証
end
end
describe 'GET /events/:id' do
let!(:event) { create :event }
let(:expected_response_object) do
{
'id' => "#{event.id}".to_i,
'title' => "#{event.title}",
'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
}
end
before do
get "/events/#{event.id}"
end
it '取得できること' do
expect(response).to have_http_status 200
expect(json).to match(expected_response_object)
end
end
describe 'POST /events' do
before do
post "/events", params: { event: { title: 'サンプルイベント' } }
end
let(:expected_response_object) do
event = Event.last
{
'id' => "#{event.id}".to_i,
'title' => "#{event.title}",
'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
}
end
it '登録できること' do
expect(response).to have_http_status 201
expect(json).to match(expected_response_object)
expect(Event.all.count).to eq 1 # DBへの反映確認
end
end
describe 'PATCH/PUT /events/:id' do
let!(:event) { create :event }
before do
patch "/events/#{event.id}", params: { event: { title: 'サンプルイベント更新' } }
end
let(:expected_response_object) do
event = Event.last
{
'id' => "#{event.id}".to_i,
'title' => "サンプルイベント更新",
'created_at' => "#{event.created_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}",
'updated_at' => "#{event.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')}"
}
end
it '更新できること' do
expect(response).to have_http_status 200
expect(json).to match(expected_response_object)
end
end
describe 'DELEET /events/:id' do
let!(:event) { create :event }
before do
delete "/events/#{event.id}"
end
it '削除できること' do
expect(response).to have_http_status 204
expect(response.body).to be_empty
expect(Event.all.count).to eq 0
end
end
end
さいごに
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!