【ITニュース解説】Helpful Settings When Running RSpec with parallel_tests
ITニュース概要
CIでRSpecを並列実行する際、`parallel_tests`を使うと高速化できるが、テストが不安定になる場合がある。テストの再現性を高めるには、全プロセスで同じseed値を使い、各プロセスが担当するファイルと実行順序を明確にすると良い。`before(:suite)`フックでファイル情報を出力することで、問題の切り分けが容易になる。
ITニュース解説
システムエンジニアを目指す上で、テストは非常に重要なスキルだ。特に、CI(継続的インテグレーション)環境でのテストは、開発の効率を大きく左右する。この記事では、RSpecというRubyのテストフレームワークを`parallel_tests`というgemを使って並列実行する際に役立つ設定について解説する。 CI環境では、テストを並列実行することで、テスト時間を短縮できる。`parallel_tests`は、複数のCPUコアを効率的に利用してテストを高速化するためのgemだ。しかし、テストが並列で実行されると、再現が難しい「flaky test(不安定なテスト)」が発生しやすくなる。これは、テストの実行順序やタイミングによって結果が変わってしまうテストのことだ。 まず、RSpecの`--seed`オプションを使う方法について説明する。`--seed`は、テストの実行順序を決定するための乱数の種を指定するオプションだ。通常、RSpecはテストを実行するたびに異なる`seed`値を生成するため、テストの実行順序も毎回変わる。しかし、特定の`seed`値を指定することで、常に同じ順序でテストを実行できるようになる。 `parallel_tests`を使ってテストを並列実行する場合、各プロセスは異なる`seed`値を持つ。そのため、特定のテストでエラーが発生した場合、そのエラーを再現するのが難しい。そこで、すべてのプロセスで同じ`seed`値を共有するように設定する。具体的には、`ruby -e "puts rand(0xffff)"`コマンドで乱数を一度生成し、その値を`bundle exec parallel_rspec -- --seed $(ruby -e "puts rand(0xFFFF)") -- spec/`のように`--seed`オプションに渡す。こうすることで、すべてのプロセスが同じ`seed`値を使用し、同じ順序でテストを実行するため、エラーの再現が容易になる。 次に、各プロセスがどのテストファイルを実行しているかを確認する方法について説明する。`parallel_tests`は、テストファイルを複数のプロセスに分散して実行するが、デフォルトではどのプロセスがどのファイルを実行しているかが出力されない。そこで、RSpecの`before(:suite)`フックを使って、各プロセスが担当するファイルの一覧を出力するように設定する。 `spec/rails_helper.rb`ファイルに以下のコードを追加する。 ```ruby RSpec.configure do |config| config.before(:suite) do files = config.files_to_run normalized = files.map { Pathname.new(File.absolute_path(it)).relative_path_from(Rails.root) } banner = "PID (#{Process.pid}) #{normalized.count} files to run:" puts [banner, *normalized].join("\n\t") end end ``` このコードを追加すると、テスト実行時に各プロセス(PID)が実行するファイルの一覧が表示される。これにより、どのプロセスでエラーが発生したのか、そのプロセスがどのファイルを実行していたのかを特定しやすくなる。 さらに、各プロセスにおけるテストファイルの実行順序を把握する方法について説明する。エラーの原因がテストファイルの実行順序に依存する場合、この情報は非常に重要になる。前の手順で、どのプロセスがどのファイルを実行しているかはわかったが、実際の実行順序まではわからなかった。そこで、`config.files_to_run`を`config.world.ordered_example_groups.map { it.file_path }`に置き換えることで、テストファイルの実際の実行順序を取得できるようにする。 ```ruby RSpec.configure do |config| config.before(:suite) do files = config.world.ordered_example_groups.map { it.file_path } normalized = files.map { Pathname.new(File.absolute_path(it)).relative_path_from(Rails.root) } banner = "PID (#{Process.pid}) #{normalized.count} files to run:" puts [banner, *normalized].join("\n\t") end end ``` この変更により、テストの実行順序が正確に表示されるようになる。エラーが発生したテストファイルの前に実行されたファイルも特定できるため、エラーの再現に必要な最小限のファイルセットを特定しやすくなる。例えば、エラーが`spec/models/foo_spec.rb`で発生し、その前に`spec/controllers/foos_controller_spec.rb`が実行されていた場合、`bundle exec rspec --seed 54242 spec/controllers/foos_controller_spec.rb spec/models/foo_spec.rb`のように、必要なファイルだけを指定してテストを実行することで、効率的にエラーを再現できる。 ただし、`world`や`ordered_example_groups`はRSpecのprivate APIであるため、RSpecのバージョンアップによって予告なく変更される可能性があることに注意する必要がある。 これらの設定を活用することで、`parallel_tests`を使った並列テスト環境でのflaky testのデバッグが大幅に効率化される。テスト結果が最初は混乱していても、適切な設定を行うことで、エラーの原因を体系的に特定し、CIパイプラインを安定させることができる。