RSpecをちょっと楽に書くためにaggregate_failuresとRequestDescriberを設定する
- dev
テストコードというものは必要だし、大切だし、適切に使えばそれはそれは心強い武器となります。が、そうはいってもやはり書くのが手間で、人間面倒だとつい疎かになりがちですね。
今回はRSpecを書くにあたってちょっと横着するための手段として、aggregate_failures
の設定とRequestDescriber
の導入をしていきます。
概要
- テストの必要性や有用性は十分に理解してるもののやはり面倒
- だから少しでも楽をしたい
- 設定の変更と、Gemを入れてちょっと楽に書こう
aggregate_failures is何?
RSpecではテストケースを書く際にit
でブロックを作ります。この1つのit
の中で複数のアサーションを書いた場合、通常だと複数の項目がパスしなかったとしても最初にパスしなかったものしかわかりません。
これが従来1つのテストケースでexpectするものは1つ、と言われていた所以です。
aggregate_failuresの設定をすると1つのit
の中で複数のexpectが期待どおりでなかったとしても、何がどうダメだったかわかりるようになります。
つまり、それまで複数書いていた同じ前提のテストケースを纏めることができるようになります。
設定方法
テストケースに指定して使う
まず、こんなケースがあるとします。
it "ユーザーの姓、名がレスポンスにあること" do
subject
expect(response.body).to include user.first_name
expect(response.body).to include user.last_name
end
この場合、最初のものでパスしないと2つめも結果は期待どおりなのか、どうなのかわかりません。
it "ユーザーの姓、名がレスポンスにあること" do
subject
aggregate_failures do
expect(response.body).to include user.first_name
expect(response.body).to include user.last_name
end
そこでこの場合 aggregate_failures
ブロックでアサーションを囲むように書きかえると両方の結果もちゃんと出るようになります。
デフォルトで効かせる
spec/spec_helper.rb
に以下のように書くだけです。
RSpec.configure do |config|
config.define_derived_metadata do |meta|
meta[:aggregate_failures] = true
end
end
これだけです。
注意点としてはRuboCopで RSpec::MultipleExpectations
が効いていると警告されてしまうのでルールを緩めておきましょう。
https://www.rubydoc.info/gems/rubocop-rspec/1.7.0/RuboCop/Cop/RSpec/MultipleExpectations
RSpec::RequestDescriber is何?
RSpec用のGemで、RSpecのRequestSpec時にDSLを拡張し、describe
に書いたリクエストがそのままSubjectとして扱われるようになります。
利点は、通常説明を書く文字列としての意味しかないdescribe
がそのまま前提の挙動になるため、説明とテストケースが意図せず整合性が崩れてしまうこともなくなりますし、記述量も減ります。
デメリットはこのGemを知らないとなぜ前提を書いていないにもかかわらず期待通りの挙動をするのかまったくわからないことです(余談ですが、昔これでかなりハマりました)
RSpec::RequestDescriberの導入
基本的にはREADMEにあるとおりですが、まずGemfileにいつものように
group :test do
gem 'rspec-request_describer'
end
と書いてbundle install
します。
次にspec/rails_helper.rb
に
RSpec.configure do |config|
config.include RSpec::RequestDescriber, type: :request
end
と追記します。これで導入はOKです。
そしてRequestSpecで
RSpec.describe 'GET /users' do
it { is_expected.to eq 200 }
end
のように書けば動作します。
describeの後に続く説明がHTTP METHODとPATHの組み合わせになっていますね。
こうすることでsubjectが自動で決まります。なのでここではis_expected
でsubjectを評価し、status 00になるかのテストとなっています。
実際どうなるか
以前使ったテストに修正を入れてみてみましょう。achievement(実績)の一覧を取得するとき、普通だと
require 'rails_helper'
RSpec.describe "Achievements", type: :request do
describe "GET /achievements" do
subject { get "/achievements" }
let(:achievements) { FactoryBot.create_list :achievement, 3 }
before do
FactoryBot.create_list(:achievement, 3)
end
it "returns 200" do
subject
expect(response.status).to eq 200
end
it "returns body with achievements" do
subject
expect(response.body).to include Achievement.first.title
end
end
end
みたいなテストがあるとします。
普通だとこのように、1テストケースで1つのexpectを書きます。前述の通り、どちらかでパスしなかった場合、両方パスしてないのか、片方だけなのかわからないため、1ケース1expectを守るように心がけます。
また、describeはあくまでただの文字列による説明です。expectするsubjectはちゃんとsubject { get "/achievements" }
と指定します。逆にいうとdescribeの内容とこのsubjectが食いちがってもコード的には問題ありません。
これを前述の設定を加えた場合、以下のように書き変えれます
require 'rails_helper'
RSpec.describe "Achievements", type: :request do
describe "GET /achievements" do
let(:achievements) { FactoryBot.create_list :achievement, 3 }
before do
FactoryBot.create_list(:achievement, 3)
end
it "returns 200 with achievement titles" do
is_expected.to eq 200
expect(response.body).to include Achievement.first.title
end
end
end
と書くことができます。
かなり短かくかけましたね。同じ前提で1つのケースとしてまとめられるので、見通しが良くなります。しかもdescribeの文字列とrequestの内容が確実に一致するため、安心です。
触れなかったこと
RequestDescriberでは、let(:params)
のようにリクエストパラメータを設定できますし、パスの変数も設定できます。このあたりはREADMEを是非参照ください。
感想
今回は2つの設定(1つはGem)の導入で少しRSpecを書く環境を楽にしました。テストを書くのがつらいとどうしてもサボりがちになり、それは割れ窓理論を呼び、カバレッジの甘いテストになります。その結果、中途半端なテストになり、それがさらにテストの必要性に懐疑的なスタンスを生み……と悪循環になっていきます。
そのためにも少しでもテストを楽に書くこと、担保すべきをきっちり担保することを心がけるのは思ったよりも大事なことだと個人的に思います。それにTDDだ書く際にもシュっとテストケースを用意できたほうが捗りますしね。