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だ書く際にもシュっとテストケースを用意できたほうが捗りますしね。