✍️

落ちるRSpecをデバッグする

2023/01/11に公開約5,600字

Rubyエンジニアとして開発をする中、ローカル環境では通っていたRSpecがCIでは通らないこと、何回か再走し直すことでようやく通るテストケースに遭遇すること、そもそもテストとして成立しているのか怪しいケースなど様々な壁にぶつかることがあるかと思います。
今回はメモとしてRSpecでテストが正常に作動しない場合にとれるデバッグ方法を載せていきます。

テスト内の変数を確認したい

プリントデバッグ

テスト内でputsを記述してあげるだけです。RSpecに限った話ではないので説明不要かと思います。

describe '#full_name'
  let(:user) { ... }
  it 'ユーザーのフルネームが表示されること' do
    puts user.last_name
    expect ...
  end
end

binding.pry

putsの時と同様にテスト内で記述します。こちらも基本的な使い方と変わりません。

describe '#full_name'
  let(:user) { ... }
  it 'ユーザーのフルネームが表示されること' do
    binding.pry # ブレークポイント
    expect ...
  end
end

テストが落ちた段階で実行を止めたい

あるテストが失敗してから後続のテストを全て待つのは面倒な時があると思います。
--fail-fastをつけることで最初にテストが失敗した段階で自動的にRSpecの実行を停止してくれます。

bundle exec rspec --fail-fast

後ろに数字を指定すると、その回数分の失敗まで実行を止めません。

bundle exec rspec --fail-fast=3 # 3回失敗するまでテストを続行する

失敗したテストだけ実行したい

正常に動かない数件のテストのために何千のテストを逐一実行するのは時間の無駄です。
--only-failuresoptionを利用することで直前に失敗したテストだけを実行することが可能です。

テスト実行前に、テストの実行結果を管理するファイルを指定します.

spec_helper.rb
RSpec.configure do |config|
  config.example_status_persistence_file_path = "spec/result.txt"
end

まずはプロジェクト全体でRSpecを流します。

bundle exec rspec 

今回は失敗したテストが数件あったとします。
すると先ほど指定したspec/result.txtには以下のように記録が残ります。

./spec/models/post_spec.rb[1:1:1:1]           | failed | 0.10028 seconds |
./spec/models/post_spec.rb[1:1:2:1]           | passed | 0.09612 seconds |
./spec/models/post_spec.rb[1:2:1:1]           | passed | 0.10231 seconds |

result.txtの中身には通ったテストと通っていないテストが記述されていることが分かります。

次に失敗したテストだけをデバッグしたい時に--only-failuresを付与すると、result.txtでfailedにマークされたテストのみが実行されます。

bundle exec rspec --only-failures

失敗していたテストが通った場合はfailedからpassedに書き変わるので、次に--only-failuresを指定した時は実行されなくなります。

指定したテストだけ実行したい

RSpecには特定のテストのみを実行させるための機能がいくつか備わっています。

ファイル名/行数を指定する

rspecコマンドの後ろに相対パスを書くとそのファイルのみ実行してくれます。

bundle exec rspec spec/models/user_spec.rb:56 # user_spec.rbの56行目のテストケースのみを実行する

特定のディレクトリ配下のテストを全て実行したい場合はディレクトリで指定できます。

bundle exec rspec spec/models # models配下のテスト全てを実行

Inclusion filters

spec_helperに以下の設定を記述します。

spec_helper.rb
RSpec.configure do |config|
  config.filter_run :focus
end

そうすることでfocus: trueと記述したテストケースだけ実行されるようになります。

it 'hoge' do
  expect ...
end

it 'huga', focus: true do # このテストのみ実行される
  expect ...
end

focusの書き方はいくつかあります。

fit 'hoge' do # it/context/describeの頭にfをつける
  expect ...
end

focus 'hoge' do
  expect ...
end

it 'hoge', :focus do
  expect ...
end

focusの消し忘れを防止するためにrubucopでRSpec::Focusを設定しておくと良いです。

ref: rspec inclusion-filters

tag option

focus filterに近い方法ですが、テストケースにtagを設定することで、実行時にtagを指定すればその設定したテストのみを走らせることができます。

it 'hoge', priority: 'high' do # タグ
  expect ...
end
bundle exec rspec test_spec.rb --tag priority: 'high' # priority: highのテストのみ実行

ref: rspec --tag options

example option

テストケースの名称を指定する方法です。

user_spec.rb
RSpec.describe User do
  describe '#full_name' do
    it 'ユーザーのフルネームが表示されること' do
      expect ...
    end
  end
  
  describe '#child?' do
    it 'ユーザの生年月日から成人済みか判定すること' do
      expect ...
    end
  end
end
bundle exec rspec spec/models/user_spec.rb --example 'User#full_name' # Userのfull_nameメソッドのみ実行
bundle exec rspec spec/models/user_spec.rb --example 'ユーザの生年月日から成人済みか判定すること' # 'ユーザの生年月日から成人済みか判定すること'という文言のテストケースを実行

ref: rspec example option

実行順序を指定したい

テストは実行順序によって通ったり通らなかったりすることもあります。
つまり「Aのテストの後にBのテストを実行すると成功するが、Bのテストの後にAのテストを実行すると失敗する」といった状態です。これはテストとして良くないので、どのような実行順序にしても成功するようにデバッグをする必要があります。
RSpecはデフォルトだとテストの実行順序はランダムに設定されていますがoptionに--orderを付与することで実行順序を指定することができます。

これを利用して、実行順序でテストが落ちた際に実行後に表示されるseed値を指定して再実行することで、失敗を再現することが可能です。

Randomized with seed 11111
bundle exec rspec test_spec.rb --order rand:11111
bundle exec rspec test_spec.rb --seed 11111 # これでもOK

もしくはrspecの結果をjsonに出力して再現する方法もあります。

bundle exec rspec \
  --seed $(jq -r '.seed' result.json) \
  $(jq -r '[.examples[].id] | join(" ")' result.json)

ref: Rails アプリケーションの不安定なテストを撲滅したい

テスト失敗時にスクリーンショットを撮りたい

system specで失敗した時の画面を確認するためにスクショを撮る方法です。
一番手軽な方法としてcapybara-screenshotというgemを使用する手段があります。
このgemは内部でテストの失敗を検知して自動的にスクリーンショットを撮ってくれます。

https://github.com/mattheworiordan/capybara-screenshot

Gemfile
group :test do
  gem 'capybara-screenshot'
end

rails_helper内でrequireするだけで設定が完了します。

require 'capybara-screenshot/rspec'

保存するディレクトリを明示的に書くこともできます。

Capybara.save_path = "/tmp/file_path"

ランダムに失敗するテストを再現したい

Rspec3.3以降から--bisectoptionというものが追加されました。
これは実行順序によって失敗するテストを最小のケース数で再現してくれるオプションです。

bundle exec rspec --bisect

実行すると以下のように失敗時の再現ができるコマンドを示してくれます。

The minimal reproduction command is:
  rspec ./spec/test_3_spec.rb[1:1] ./spec/test_1_spec.rb[1:1] --seed 11111

後ろに=verboseをつけると失敗時の再現コマンドを特定するまでの様子を出力します。

bundle exec rspec --bisect=verbose
Bisect started using options: "--seed 11111" and bisect runner: :fork
Running suite to find failures... (0.124353 seconds)
 - Failing examples (1):
    - ./spec/test_1_spec.rb[1:1]
 - Non-failing examples (5):
    - ./spec/test_2_spec.rb[1:1]
    - ./spec/test_3_spec.rb[1:1]
    - ./spec/test_4_spec.rb[1:1]
 ...

ref: rspec --bisect

Discussion

ログインするとコメントできます