はじめに
自分がこれまで API 向けの RSpec を書いてきて、これは良いな!と思った記述の仕方をご紹介します。
うまく活用することで、わかりやすく簡潔な記述ができるかと思います。
RSpec と Ruby のバージョン
- RSpec 3.8.0
- Ruby 2.4.0
subject
を活用する
条件が異なるテストがあり、同じ API 呼び出し (before { get '/users' }
) が
複数箇所に現れてしまったそんなとき!
describe '#index' do
context 'with 1 user' do
let!(:user) { create(:user) }
before { get '/users' }
it { expect(response.status).to eq 200 }
end
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
before { get '/users' }
it { expect(response.status).to eq 200 }
end
end
下記のようにした場合、API 呼び出しが条件設定前(ユーザー作成前)に実行されるので、
意図したテストになりません
describe '#index' do
before { get '/users' } # ユーザー作成前に実行されてしまう
context 'with 1 user' do
let!(:user) { create(:user) }
it { expect(response.status).to eq 200 }
end
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it { expect(response.status).to eq 200 }
end
end
subject
& is_expected
を活用すると、DRY にできます!
また、どの API 呼び出しに対するテストなのかも分かりやすくなると個人的に感じています
describe '#index' do
subject { get '/users' }
context 'with 1 user' do
let!(:user) { create(:user) }
it { is_expected.to eq 200 } # ここで subject が評価される
end
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it { is_expected.to eq 200 } # ここで subject が評価される
end
end
注意点
subject
が評価されるのは is_expected
が呼ばれたときなので、
response
を確認する場合には順序にご注意を。
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body) # subject が評価されている必要あり
expect(response_json.length).to eq 2
end
end
change
マッチャの活用
「API 呼び出しによって、何かの変化を確認したい!」場合には、change
マッチャ!
例として、ユーザー数の変化を確認するテストを作成してみます。
愚直に書くと、下記のように、API 呼び出し前後での User.count
の値を確認することに…
describe 'POST /users' do
context 'with correct parameters' do
let(:params) { { name: 'name', email: 'hoge@hoge' } }
it 'should create 1 user' do
expect(User.count).to eq 0
post '/users', params: params
expect(User.count).to eq 1
end
end
context 'with empty parameters' do
let(:params) { {} }
it 'should not create any user' do
expect(User.count).to eq 0
post '/users', params: params
expect(User.count).to eq 0
end
end
end
change
マッチャを活用すると下記のように記述できます。
何によって、何の値が、どのように変化することを期待しているのか、が分かりやすいのではないでしょうか
(ただし、change
マッチャは expect
が長くなりやすいので、諸刃の剣感があります)
describe 'POST /users' do
context 'with correct parameters' do
let(:params) { { name: 'name', email: 'hoge@hoge' } }
it 'should create 1 user' do
expect { post '/users', params: params }.to change(User, :count).from(0).to(1)
end
end
context 'with empty parameters' do
let(:params) { {} }
it 'should not create any user' do
expect { post '/users', params: params }.not_to change(User, :count).from(0)
end
end
end
subject
と組み合わせる場合には、subject
をブロックにする必要があります。
スッキリしますね
describe 'POST /users' do
subject { -> { post '/users', params: params } }
context 'with correct parameters' do
let(:params) { { name: 'name', email: 'hoge@hoge' } }
it { is_expected.to change(User, :count).from(0).to(1) }
end
context 'with empty parameters' do
let(:params) { {} }
it { is_expected.not_to change(User, :count).from(0) }
end
end
shared_context
+ include_context
複雑な前提条件やよく使う前提条件を shared_context
として定義しておくことで、
include_context
で読み込むことができます。
例えば、以下のように複数のレコードが絡むような前提条件があると、見通しが少し悪くなってしまいます。
context 'test case A' do
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let!(:another_user_follow) { create(:relationship, follower_id: another_user.id, followed_id: user.id) }
it { is_expected.to eq 200 }
end
context 'test case B' do
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let!(:another_user_follow) { create(:relationship, follower_id: another_user.id, followed_id: user.id) }
# (他の条件)
it { is_expected.to eq 200 }
end
前提条件を share_context
で定義しておき、必要なテストで include_context
を使い読み込むことで、
実際のテストの見通しが良くなります
shared_context 'followed_user' do
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let!(:user_follow) { create(:relationship, follower_id: another_user.id, followed_id: user.id) }
end
context 'test case A' do
include_context 'followed_user'
it { is_expected.to eq 200 }
end
context 'test case B' do
include_context 'followed_user'
# (他の条件)
it { is_expected.to eq 200 }
end
よく使う前提条件は、spec/support/shared_contexts/
以下に定義しておくと、
他のテストで利用できるので便利です。
spec/support
以下に置いて読み込む場合には、rails_helper.rb
に下記のような記述が必要です。
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
include を活用する
request spec でレスポンス中のパラメーターの値まで確認することは、場合によってはやりすぎです
ただし、API として最低限、レスポンスのキーが存在するかは確認しておきましょう。
describe '#index' do
subject { get "/users/#{user.id}" }
context 'with 1 user' do
let(:user) { create(:user) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to include('id', 'name')
end
end
end
レスポンスが配列の場合には、all
を使うことで、include
を各要素に適用できます
describe '#index' do
subject { get '/users' }
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
end
end
end
shared_examples/shared_examples_for
条件は異なるけれども、同じ結果を期待する場合には、shared_examples
を使うことで DRY にできます
(shared_examples_for
とも書くことができます)
describe '#index' do
subject { get '/users' }
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
end
end
context 'with 3 users' do
let!(:users) { create_list(:user, 3) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
end
end
end
shared_examples
の内容を it_behaves_like
で使用することができます。
describe '#index' do
subject { get '/users' }
shared_examples 'return proper response keys' do
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
end
end
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it_behaves_like 'return proper response keys'
end
context 'with 3 users' do
let!(:users) { create_list(:user, 3) }
it_behaves_like 'return proper response keys'
end
end
引数を使うこともできるので、下記のように似ているけれども微妙に異なるものも…
describe '#index' do
subject { get '/users' }
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
expect(response_json.length).to eq 2
end
end
context 'with 3 users' do
let!(:users) { create_list(:user, 3) }
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
expect(response_json.length).to eq 3
end
end
end
下記のように shared_examples
で書くことができます
describe '#index' do
subject { get '/users' }
shared_examples 'return proper response keys' do |user_count|
it do
is_expected.to eq 200
response_json = JSON.parse(response.body)
expect(response_json).to all(include('id', 'name'))
expect(response_json.length).to eq user_count
end
end
context 'with 2 users' do
let!(:users) { create_list(:user, 2) }
it_behaves_like 'return proper response keys', 2
end
context 'with 3 users' do
let!(:users) { create_list(:user, 3) }
it_behaves_like 'return proper response keys', 3
end
end
shared_examples の注意点
shared_examples
は便利なのですが、使いすぎると見通しが悪くなってしまうこともあると感じています
うまく活用して、全体として理解しやすい記述を目指したいですね
カスタムマッチャを活用する
カスタムマッチャを活用することで、期待値判定の記述を簡潔にすることができます。
特に、独自のエラーレスポンスを定義している場合には、カスタムマッチャ導入で捗りそうです
例えば、下記のような、エラーレスポンスを確認するテストがあるとします。
describe '#index' do
subject { get '/users/0' }
context 'with no user' do
it do
is_expected.to eq 404
response_json = JSON.parse(response.body)
expect(response_json['error']['code']).to eq 111
expect(response_json['error']['message']).to eq 'not found'
end
end
end
エラーレスポンスを確認するカスタムマッチャを定義しておくと、下記のように書くことができます
describe '#index' do
subject { get '/users/0' }
context 'with no user' do
it do
is_expected.to eq 404
expect(response).to be_error_json(code: 111, message: 'not found')
end
end
end
カスタムマッチャの定義は下記のようになっています。
RSpec::Matchers.define :be_error_json do |code:, message:|
match do |actual|
json_response = JSON.parse(actual.body)
expect(json_response['error']['code']).to eq code
expect(json_response['error']['message']).to eq message
end
end
カスタムマッチャは、spec/support/matchers.rb
などに定義すると良いでしょう
spec/support
以下に置いて読み込む場合には、rails_helper.rb
に下記のような記述が必要です。
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
おわりに
以上、APIのテストで活用したい記述方式でした!
RSpec の記述方式は、チームによって方針があったり、
良い感じに記述できた!と思ったら、後から見たら分かりづらくなっていたり…ということがあるので、
過度に DRY にせず、便利な機能も用法用量を守って適切に使うのが良いかと思います
今回ご紹介した以外にも便利な機能はたくさんあるので、ぜひ活用して、楽しいテストライフをお過ごしください〜。