- Ruby / Rails関連
READ MORE
初めまして、takanekoと申します。
入社して以来2年くらい「ブログ書きます!」と言いつつすっぽかし続けたダメWebエンジニアです。
「ぼくのあーるすぺっくのかきじゅん」(僕のRSpecの書き順)です。
RSpecの書き方わからない、という方のために、0から書き上げる際の参考になればと思い書きました。
RSpecにはModel Spec、Feature Spec、Request Spec、System Specなど色々種類がありますが、今回はRequest Specを書いていきます。APIとかのテストをする際に使うSpecです。
書き順がメイントピックなので、細かい実装の妥当性とかは無視して、以下のような簡単な仕様のユーザ登録APIでSpecを書きたいと思います。
POST /users(format: :json)
email
, password
, password_confirmation
をPOSTしてユーザ作成200 OK
とユーザのidをjsonで返却する400 Bad Request
を返却する400 Bad Request
を返却する401 Unauthorized
を返却するspec/requests/users_create_spec.rb
に空のSpecを書いてスタートしましょう。
require 'rails_helper'
RSpec.describe 'POST /users', type: :request do
# やるぞ
end
context
とit
だけ並べて、自然言語で仕様を書き起こしましょう。
RSpec.describe 'POST /users', type: :request do
context '全てのパラメータが揃っている場合' do
it '200 OKを返す'
it '成功時のJSONレスポンスを返す'
it 'ユーザを登録する'
end
context 'emailパラメータが不足している場合' do
it '400 Bad Requestを返す'
it 'パラメータ不正のJSONレスポンスを返す'
it 'ユーザを登録しない'
end
context 'emailが既に登録されている場合' do
it '400 Bad Requestを返す'
it 'email重複エラーのJSONレスポンスを返す'
it 'ユーザを登録しない'
end
context '管理者アカウント未ログインの場合' do
it '401 Unauthorizedを返す'
it 'ユーザを登録しない'
end
end
どうでもいいけど、結合テスト仕様書ってこんな感じですよね。
テストを書く上で一番時間のかかる部分は前提条件です。(今回は簡単な仕様のテストなのでさほど感じませんが)
その間テストが形にならず、何かやったという気になりません…。
でも、it
に自然言語で記述した内容をexpect
に起こすのは流れ作業でできそうです。expect
から書いていきましょう。
expect
はテストのゴールなので、それが明確になるとちょっとだけやる気が出てくるような気がします(弱気)。
RSpec.describe 'POST /users', type: :request do
context '全てのパラメータが揃っている場合' do
it '200 OKを返す' do
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
expect(response).to have_http_status(:ok)
end
it '成功時のJSONレスポンスを返す' do
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
expect(JSON.parse(response.body)).to eq(
'user' => { 'id' => User.find_by(email: 'test@bpsinc.jp').id }
)
end
it 'ユーザを登録する' do
expect do
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
end.change(User, :count).by(1)
# パスワードを暗号化していない :masakari:
expect(User.find_by(email: 'test@bpsinc.jp')).to have_attributes(password: 'hogehoge')
end
end
...
end
ちょっとでも弾みをつけたい時には、書くのが簡単なテストケースから手をつけましょう。
例えば、ログアウトした状態で401を返すテストは楽です。ログインコード書かなくて良いですもんね。
RSpec.describe 'POST /users', type: :request do
...
context '管理者アカウント未ログインの場合' do
it '401 Unauthorizedを返す' do
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
expect(response).to have_http_status(:unauthorized)
end
it 'ユーザを登録しない' do
expect do
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
end.change(User, :count).by(0)
end
end
end
expect
を書いた時点で完成していますね。
この時点でSpecを動かしてみて、テスト成功を確認しましょう。細かく満足感を得ていくのは大事です。
bundle exec rspec spec/requests/users_create_spec.rb -e '管理者アカウント未ログインの場合'
before
に前提条件を書いていきます。
例えば、正常系はログインするだけなので簡単ですね。
RSpec.describe 'POST /users', type: :request do
context '全てのパラメータが揃っている場合' do
before { sign_in create(:administrator) } # createはFactoryBotのメソッドです
it '200 OKを返す'
it '成功時のJSONレスポンスを返す'
it 'ユーザを登録する'
end
...
end
メール重複による失敗時の場合は、以下のような前提条件になりますね。
RSpec.describe 'POST /users', type: :request do
...
context 'emailが重複している場合' do
before do
# email重複ユーザを作る
create(:user, email: 'test@bpsinc.jp')
# サインイン
sign_in create(:administrator)
end
it '400 Bad Requestを返す'
it 'email重複エラーのJSONレスポンスを返す' do
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
expect(JSON.parse(response.body)).to eq(
'error' => "そのメールアドレスは使用されています(user_id=#{User.find_by(email: 'test@bpsinc.jp').id})"
)
end
it 'ユーザを登録しない'
end
...
end
今回は書きませんが、GET系のAPIだとここでデータを作っていくのが大変です…。頑張りましょう!
全てのテストを通すまで、繰り返していきましょう。
この記事では成功を期待する時だけ実行していますが、失敗する場合でも、期待する失敗になっているかを随時確認したほうがいいです。
贅沢にもテストコードをリファクタリングしていきましょう。
before
とit
が分かれていると読みやすいですが、ブロックが分かれていてローカル変数アクセスできないので不便ですね(盲信)。
一緒にしちゃいましょう。
RSpec.describe 'POST /users', type: :request do
...
context 'emailが重複している場合' do
it 'email重複エラーのJSONレスポンスを返す' do
# email重複ユーザを作る
user = create(:user, email: 'test@bpsinc.jp')
# サインイン
sign_in create(:administrator)
post '/users', params: { email: 'test@bpsinc.jp', password: 'hogehoge', password_confirmation: 'hogehoge' }, headers: { 'ACCEPT' => 'application/json' }
expect(JSON.parse(response.body)).to eq(
'error' => "そのメールアドレスは使用されています(user_id=#{user.id})"
)
end
...
end
ここで「let
を使うのがいいのでは?」という質問が出そうなので先に弁明しておきます。
let
は以前ひどい目にあって以来、Model Spec以外では基本的に書かないようになりました。
let
の適切な書き方はご説明できないので、別の解説記事を参考になさってください💦。
context
とit
は合わせて一つの文章なので、一緒にした方が読みやすい気がします(盲信)。
RSpec.describe 'POST /users', type: :request do
it '全てのパラメータが揃っている場合, 200 Okを返す'
it '全てのパラメータが揃っている場合, 成功時のJSONレスポンスを返す'
it '全てのパラメータが揃っている場合, ユーザを登録する'
...
end
これでどんなにSpecが長くなっても、エディタの画面からcontext
が出て行って「あれ、これのcontext
なんだっけ?」となりません。ファイル分割しろって話ですが。
短いのでついit
を書いてしまうのですが、主語(subject
)がないので何を指しているかわかりませんね。
同じ用途で使えるexample
でもいいけど、test
とかの方がわかりやすくないですか?
RSpec::Core::Configuration#alias_example_toでexample
に別名をつけられます。
spec/spec_helper.rb
に以下を追記します。
RSpec.configure do |config|
config.alias_example_to :test
...
end
今までのテストコードもit
からtest
に置き換えます。
RSpec.describe 'POST /users', type: :request do
test '全てのパラメータが揃っている場合, 200 Okを返す'
test '全てのパラメータが揃っている場合, 成功時のJSONレスポンスを返す'
test '全てのパラメータが揃っている場合, ユーザを登録する'
...
end
これで完成!!
Minitest派だけどアンチではないです。ほんとです!
嫌いというより、格好良く書こうとして読みづらいテストコードになることが多いよなぁ、というイメージがあります。
格好良く書きたくなる魔力が、RSpecにはあるんですかね。
Minitestだから必ず良くなるとも思っていないです。単純な書き方しか出来ない方が、単調なテストコードになる確率が高そう、くらいの気持ちです。
正直、嫌いです。
自分がlet
を書いてしまうことで、そのlet
が自分以外の人によってcontext
の階段を駆け上がっていき、いつしかSpecのtop levelにいる悪夢を見ます(大げさ)。
遅延評価(let
)、即時実行(let!
)とかもそれに気づかずにテストがfailしたり、前提条件を勘違いしてしまったりして苦手なんですよね…。
私が病的に嫌いなだけなので、使うべき適切な場面はあると思います。
subject
が使いやすいModel Specとか。
基本的にテストコードの冗長は問題ないと考えてます。
特に前提条件before
と期待値expect
に関しては、むやみに共通化することで、後から書く人がその共通部分にひきずられて窮屈な or 無理のあるテストコードしか書けなくなってしまいがちです。
ただ、この記事は読みやすさ重視で冗長にしているので、全部このままがいいとは思ってません。例えば、post
部分とかは便利メソッドがあるといいかも。
半分が煽りっぽいネタ記事で申し訳ありません。
普段RSpecしか書かないのですが、Minitestを書きたいのでその闇が出てきてしまいました。
ここまで読んでいただき、ありがとうございました。