初めまして、takanekoと申します。
入社して以来2年くらい「ブログ書きます!」と言いつつすっぽかし続けたダメWebエンジニアです。
RSpecえかきうた?
「ぼくのあーるすぺっくのかきじゅん」(僕のRSpecの書き順)です。
RSpecの書き方わからない、という方のために、0から書き上げる際の参考になればと思い書きました。
Specを書き始めましょう
RSpecにはModel Spec、Feature Spec、Request Spec、System Specなど色々種類がありますが、今回はRequest Specを書いていきます。APIとかのテストをする際に使うSpecです。
書き順がメイントピックなので、細かい実装の妥当性とかは無視して、以下のような簡単な仕様のユーザ登録APIでSpecを書きたいと思います。
API仕様
- URL
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
どうでもいいけど、結合テスト仕様書ってこんな感じですよね。
expectを書きましょう
テストを書く上で一番時間のかかる部分は前提条件です。(今回は簡単な仕様のテストなのでさほど感じませんが)
その間テストが形にならず、何かやったという気になりません...。
でも、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をスッキリさせる
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をスッキリさせる
context
とit
は合わせて一つの文章なので、一緒にした方が読みやすい気がします(盲信)。
RSpec.describe 'POST /users', type: :request do
it '全てのパラメータが揃っている場合, 200 Okを返す'
it '全てのパラメータが揃っている場合, 成功時のJSONレスポンスを返す'
it '全てのパラメータが揃っている場合, ユーザを登録する'
...
end
これでどんなにSpecが長くなっても、エディタの画面からcontext
が出て行って「あれ、これのcontext
なんだっけ?」となりません。ファイル分割しろって話ですが。
subjectがないのにitとは
短いのでつい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派)だな、オメー?
Minitest派だけどアンチではないです。ほんとです!
RSpec嫌いなの?
嫌いというより、格好良く書こうとして読みづらいテストコードになることが多いよなぁ、というイメージがあります。
格好良く書きたくなる魔力が、RSpecにはあるんですかね。
Minitestだから必ず良くなるとも思っていないです。単純な書き方しか出来ない方が、単調なテストコードになる確率が高そう、くらいの気持ちです。
let嫌いなの?
正直、嫌いです。
自分がlet
を書いてしまうことで、そのlet
が自分以外の人によってcontext
の階段を駆け上がっていき、いつしかSpecのtop levelにいる悪夢を見ます(大げさ)。
遅延評価(let
)、即時実行(let!
)とかもそれに気づかずにテストがfailしたり、前提条件を勘違いしてしまったりして苦手なんですよね...。
私が病的に嫌いなだけなので、使うべき適切な場面はあると思います。
subject
が使いやすいModel Specとか。
それにしても冗長すぎない?
基本的にテストコードの冗長は問題ないと考えてます。
特に前提条件before
と期待値expect
に関しては、むやみに共通化することで、後から書く人がその共通部分にひきずられて窮屈な or 無理のあるテストコードしか書けなくなってしまいがちです。
ただ、この記事は読みやすさ重視で冗長にしているので、全部このままがいいとは思ってません。例えば、post
部分とかは便利メソッドがあるといいかも。
最後に
半分が煽りっぽいネタ記事で申し訳ありません。
普段RSpecしか書かないのですが、Minitestを書きたいのでその闇が出てきてしまいました。
ここまで読んでいただき、ありがとうございました。