RSpecを並列実行するgemを作っている話

  • 58
    いいね
  • 0
    コメント

これは Ruby アドベントカレンダー 24 日目の記事です。

Railsを長く開発していると機能を追加していくにつれてテストコードも肥大化し、初めのうちは一瞬で終わっていたrspecも気がつけば数十分かかるようになっていたということも多いと思います。テストをCIで回していると、結果が得られるまで作業が止まることになるので、テスト時間の肥大化は結構大きなインパクトを持ってきます。

テストの中にボトルネックがある場合それを解消することである程度の高速化ができますが、純粋にテストの数が多いということになると、全てのテストを実行するのを諦めないのであれば、テストを並列に実行するのが高速化のアプローチとなります。

テストを並列実行するgem

テストを並列に実行するgemはすでに世の中にいくつもあります。

rrrspec

Cookpad社が作っているrrrspecはRSpecを複数サーバで分散実行します。CircleCIなどのCIサービス上で使うのは難しいです。(多分)

parallel_tests

parallel_testsは複数プロセスでテストを分散処理するgemです。この手のgemの中でおそらく一番githubスターを集めているのがこれです。このgemの最大の問題は、テスト実行の最初の時点で各プロセスにテストファイルを割り当ててしまうことです。ファイル間で実行時間に偏りがある場合、1つのプロセスだけが走り続けてテスト実行時間が伸びる可能性があります。

test-queue

test-queueはparallel_testsと同様に複数プロセスでテストを実行します。parallel_testsとは異なり、実行時に順次テストを配布していくのでテスト時間の偏りは発生しません。また、複数サーバにまたがってテストを実行する機能もあります。

機能的には必要なものを全て持っていて素晴らしいのですが、使ってみたらうまく動かず、デバッグしようとコードを見たら、いかにもCプログラマが手続き的に書き下したような感じでなかなか読むのが辛い。しかも頑張って読み込んでいっても問題がわからないし、これは自分で書き直した方が早いのでは?という気がしてきました。

rspec-parallelrspec_parallel

rspec-parallelとrspec_parallelはRSpec専用の複数スレッドでテストを分散処理するgemです。残念ながら両方とも積極的には開発されていないです。またE2Eテストにpoltergeistを使っている都合で一部DBをtruncateが必要な箇所があるためスレッドだと困るのと、CI環境のコアをちゃんと使い切りたいとか、そういう都合もあって使えませんでした。

parallel-rspec

というわけで既存のgemがうまく動かなかった+デバッグが難しかったため、 parallel-rspecという新しいRSpecを並列実行するgemを作っています。特徴としては

  • 単一のマシンの上で動く
  • プロセスベース
  • RSpecだけに対応して
    • 余分な複雑さを排除
    • パフォーマンス最適化
  • RSpecっぽい使い勝手
  • OOP的に書く

使い方

gemをインストールするとparallel-rspecコマンドがインストールされるので、これをrspecコマンドの代わりに使うだけです。

parallel-rspec spec

これでspecディレクトリ以下の *_spec.rb が全て分散実行されます。この例では一切引数が渡されていませんが、rspecコマンドに渡せる全てのオプションを指定することができます。ただし複数プロセスで動いているので、一部のオプションは互いに上書きしあってうまく動かない可能性があります。また並列度は自動的に実行環境のコア数になります。

parallel-rspecコマンドはspec/parallel_spec_helper.rbが存在する場合、それを自動的に読み込みます。このファイルを使って挙動の設定をすることができます。例えばRailsを使っているとして、並列度を4にして、各プロセスごとに異なるDBを使う設定はこんな感じです。

RSpec::Parallel.configure do |config|
  config.concurrency = 4

  config.after_fork do |worker|
    # config/database.ymlのtestで指定したdb名の末尾にそれぞれ0~3がつく
    ActiveRecord::Base.configurations["test"]["database"] << worker.number.to_s
    ActiveRecord::Base.establish_connection(:test)
  end
end

このようにプロセスごとにDBを個別に用意するのはRailsだと多いので、専用のrakeタスクが用意されています。以下のコマンドを実行すると4つのDBが作られます。引数で並列度を指定していますが、省略した場合コア数になります。

rake "db:test:prepare_sequential[4]"

終わりに

まだ実プロジェクトで使えるような段階ではないので注意が必要です。アドベントカレンダーが空いていたので埋めてみました。開発の励みになるのでおとしだまがわりにGitHubスターをください :pray:

この投稿は Ruby Advent Calendar 201624日目の記事です。