RSpec で seed 固定して flaky test をやっつけましょう
この記事について
RSpec で書いたテストを CI で実行する際、ときどき失敗するテストがあったりしないでしょうか? いわゆる flaky test と呼ばれるものです。
これに何も考えずに re-run により成功を観測してヨシッ! みたいな運用しているのはもったいないので直しましょうという内容です。
config.order
よび Kernel.srand
の設定
といっても大したことでなく、 RSpec により生成される設定ファイルの以下の箇所を有効にしましょうという話でしかありません。
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
=end
この設定を有効にしておくと、ランダムに失敗するテストをあとから再現させることができます。
以下は日本語でコメントの箇所を訳したものです。
順序の依存関係を明らかにするために、スペック(テスト)をランダムな順序で実行します。
もし順序の依存関係が見つかり、デバッグしたい場合は、各実行後に表示されるシード値を指定することで、順序を固定できます。
--seed 1234
このプロセスでグローバルなランダム化を行うには、
--seed
コマンドラインオプションを使用してください。
ランダム化に関連するテストの失敗を再現する際に、失敗を引き起こしたものと同じ--seed
の値を指定することで、決定論的に再現することができます。
ちなみに generate した直後のファイルの状態では =begin
から =end
までの間に記載されていると思われます。
つまりコメントアウトされているので、有効化する場合はこれらの外に配置してください。
=begin
ここは
# 全部コメント
です
=end
flaky test 対応の実践
以下では、試しにどんな流れで flaky test が対応されるかを実践しています。
rails new からの準備
# rails アプリケーション作成
$ rails new --skip-test rspec-rails-test
$ cd rspec-rails-test/
# rspec-rails の導入および初期設定
$ bundle add rspec-rails --group development,test
$ rails generate rspec:install
続いて、例の config.order
よび Kernel.srand
に関する設定を有効化します。
=begin
=end
の間に配置されているので、これを外に出す形に調整しましょう。
# Print the 10 slowest examples and example groups at the
# end of the spec run, to help surface which specs are running
# particularly slow.
config.profile_examples = 10
+=end
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
-=end
end
試しにここで bundle exec rspec
を実行してゼロ件のテストが成功することを確認しておきます。
$ bundle exec rspec
No examples found.
Randomized with seed 37597
Finished in 0.00031 seconds (files took 0.09676 seconds to load)
0 examples, 0 failures
Randomized with seed 37597
$
モデルを作ってテストも記述
テストの対象であるモデルをひとつ作ってみます。
年月を意味する、 YearMonth
モデルとして、属性は year
と month
を有します。
それぞれに簡単にバリデーションも記載します。
only_integer
、つまり整数のみを許容し、 month
の方にはさらに 1-12 の範囲のみを許容する形に構成します。
class YearMonth
include ActiveModel::Model
include ActiveModel::Attributes
attribute :year, :integer
attribute :month, :integer
validates :year, numericality: { only_integer: true }
validates :month, numericality: { only_integer: true, in: (1..12) }
end
つづいて YearMonth
モデルに対するテストを用意します。
といっても、作成したモデルが valid?
であることを確認するだけの簡単なものです。
require "rails_helper"
RSpec.describe YearMonth do
subject { YearMonth.new(attributes) }
let(:attributes) { { year:, month: } }
let(:year) { rand(2100) }
let(:month) { rand(12) }
it do
expect(subject.valid?).to be_truthy
end
end
テストを実行
そして bundle exec rspec
でテストが動作することを確認します。
$ bundle exec rspec
Randomized with seed 54242
.
Finished in 0.0191 seconds (files took 0.92096 seconds to load)
1 example, 0 failures
Randomized with seed 54242
$
たまに失敗するのを観測する
実はこのテストはときどき失敗することがあります。
何回か実行すると、以下のように異常終了が確認できるはずです。
プロダクトのコードで、元気に正常終了している CI が時々失敗を通知してくるイメージですね。
$ bundle exec rspec
Randomized with seed 30925
F
Failures:
1) YearMonth is expected to be truthy
Failure/Error: expect(subject.valid?).to be_truthy
expected: truthy value
got: false
# ./spec/models/year_month_spec.rb:11:in 'block (2 levels) in <top (required)>'
Finished in 0.01313 seconds (files took 0.50544 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/year_month_spec.rb:10 # YearMonth is expected to be truthy
Randomized with seed 30925
$
失敗を再現する
失敗したときのメッセージについて、以下の箇所に注目します。
Randomized with seed 30925
この、シード値 (今回の場合) 30925
がポイントになります。
この値を、コマンドラインオプション --seed
に指定して実行すると先程の乱数の結果が再現できるため確実に再現できるようになります。
$ bundle exec rspec --seed 30925
Randomized with seed 30925
F
Failures:
1) YearMonth is expected to be truthy
Failure/Error: expect(subject.valid?).to be_truthy
expected: truthy value
got: false
# ./spec/models/year_month_spec.rb:11:in 'block (2 levels) in <top (required)>'
Finished in 0.02331 seconds (files took 0.90374 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/year_month_spec.rb:10 # YearMonth is expected to be truthy
Randomized with seed 30925
$
デバッグによる原因特定
再現さえできれば、あとは素直にデバッグするだけです。
ここでは愚直に puts
を入れて attributes
の状態を確認しています。
require "rails_helper"
RSpec.describe YearMonth do
subject { YearMonth.new(attributes) }
let(:attributes) { { year:, month: } }
let(:year) { rand(2100) }
let(:month) { rand(12) }
it do
+ puts "attributes: #{subject.attributes.inspect}"
expect(subject.valid?).to be_truthy
end
end
$ bundle exec rspec --seed 30925
Randomized with seed 30925
attributes: {"year" => 1913, "month" => 0}
F
Failures:
1) YearMonth is expected to be truthy
Failure/Error: expect(subject.valid?).to be_truthy
expected: truthy value
got: false
# ./spec/models/year_month_spec.rb:12:in 'block (2 levels) in <top (required)>'
Finished in 0.01298 seconds (files took 0.50863 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/year_month_spec.rb:10 # YearMonth is expected to be truthy
Randomized with seed 30925
$
month
の値が 0
(ゼロ) ですね。
attributes: {"year" => 1913, "month" => 0}
rannd(12)
の箇所が間違っていました。
この場合、 0 から 11 までの範囲の乱数になってしまっています。
let(:month) { rand(12) }
> 10000.times.map { rand(12) }.uniq.sort
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
あとは簡単、直してしまいましょう
原因のしっぽを掴んだので、あとはシュッ!と直してしまえば解決です!
require "rails_helper"
RSpec.describe YearMonth do
subject { YearMonth.new(attributes) }
let(:attributes) { { year:, month: } }
let(:year) { rand(2100) }
- let(:month) { rand(12) }
+ let(:month) { rand(1..12) }
it do
expect(subject.valid?).to be_truthy
end
end
> 10000.times.map { rand(1..12) }.uniq.sort
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
あとはコミットコメントを積んで CI に流すなりしてしまいましょう!
git commit -m 'fix flaky test'
まとめ
- RSpec での
spec/spec_helper.rb
でconfig.order = :random
およびKernel.srand config.seed
を有効にすると flaky test 退治が捗ります。 - 失敗したときの出力からシード値を確認し、
--seed
オプションに指定することで失敗を再現できます - 割れ窓にせずコツコツ直し、本当に捕まえるべき失敗を認知しやすい状態を作りましょう
Discussion