Railsで動くRspecのテストが遅いのでparallel_testsを導入してみる

--- ここからテンプレ --- 仕事で関わっているRailsアプリではPRの最新コミットのごとにCIが周る。
CIはcloneしたRailsアプリに対してRspecを実行し、その結果がリポジトリに反映される。

という、ごく普通のフローが存在するが、リポジトリが成長すると共にテストもそれなりに数を増やし、共にRspecの実行時間もそれなりに伸び、つまりCIの実行時間も伸びていく。

CIの結果を頼りにレビューをする方も居るだろうし、CIの結果がわからないとマージはできないように、CIの結果を待たなければいけないケースはそれなりに発生すると思っている。

そういった待ち時間は時間の程度にも寄るが、他の作業の妨げになるかもしれない。他の作業の妨げになるのであれば、待ち時間は短ければ短いほど良く、つまりテストの実行時間は早ければ早いほど良いということだろう。

--- ここまでテンプレ ---

ただあまり大きな労力はかけたくない(めんどくさい)ので、まずは大きく有効そうな手立てから試していきたい。

parallel_tests とは

github.com

簡単に言えば

  • CPUのコア毎にRspecのプロセスを立ち上げてテストを並列実行する

というもの

癖はあるようで、

  • プロセスごとに独立したDBが必要(rails db:setup のように複数のDBを作るためのrakeタスクは存在している)
  • 実行時にファイルサイズを鑑みていて、各プロセスに渡るspecファイルの合計ファイルサイズが近くなるように振り分けられる
    • 小さなファイルサイズでも激重なテストケースが入っている場合は考慮されない

ということらしいが、導入はそんなに苦なものではないようなので一旦導入してみた

導入方法

Gemfileに以下を追加してbundle install

# Gemfile
gem 'parallel_tests', group: %i[development test]
$ bundle install

test用のdatabaseを作る

# config/database.yml
test:
  database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>
# 環境変数 `TEST_ENV_NUMBER` に実行されるプロセスの番号が入ってくるらしい
$ rails parallel:setup

これでコア数に応じて

  • yourproject_test
  • yourproject_test2
  • yourproject_test3
  • yourproject_test4

みたいに4つのdatabaseができるはず

おそらくお手持ちのRailsアプリケーションはmemcachedやRedisなどを噛んでいると思うが、その場合は先程書いた環境変数 TEST_ENV_NUMBER を使ってそれぞれのネームスペースを切ると良さそう(以下参照2つ)

# config/environments/test.rb
config.cache_store = :memory_store, { namespace: "yourproject_test#{ENV['TEST_ENV_NUMBER']}" }
# mastodonの例
# spec/rails_helper.rb
Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current)

あと何故かcache_storeがfile_store(未指定時デフォ)の場合、テストが途中で死ぬ(下記エラー)ことがあった
多分 Rails.cache.clear! で別プロセスが参照するキャッシュが消えたんだと思う、ここをうまく切るためにnamespaceを儲けようとしたが結局うまく行かなかったので一旦はmemory_storeを使うように変更している

Errno::ENOENT:
     #   No such file or directory @ apply2files - /Users/naari3/src/github.com/naari3/some_project/tmp/cache/FB1/000/.permissions_check.70365749710360.44558.326263
     #   ./app/models/user.rb:25:in `info

実行

実行する

$ rails parallel:spec
4 processes for 224 specs, ~ 56 specs per process

Randomized with seed 58546
....
Randomized with seed 39588
..
Randomized with seed 40142
......................
Randomized with seed 39700
........
(省略)
Coverage report generated for (1/4), (2/4), (3/4), (4/4), RSpec to /Users/naari3/src/github.com/naari3/some_project/coverage. 4352 / 5499 LOC (79.14%) covered.

1707 examples, 0 failures, 1 pending

Took 171 seconds (2:41)

このマシンには4コア積まれているので4プロセス立ち上がってそれぞれ実行された

なお普通にRspecを実行した際のログはこちら

$ bundle exec rspec
(省略)
Finished in 6 minutes 35 seconds (files took 5.08 seconds to load)
1707 examples, 0 failures, 1 pending

開発マシンで裏で色々立ち上がっている中、parallel_tests経由で並行に実行するだけで 1/2 くらいの実行時間にすることができた

実際はおそらくエージェントとかDockerプロセスとか最低限のものだけが立ち上がっているCIの環境上で動かされるのでコア数分だけ数倍速になるんだろうなぁと思っている(まだ動かしてない)