patorashのブログ

方向性はまだない

1つのspecファイルをparallel_testsで並列処理する方法

parallel_testsでテストの並列化していくのが好きなのですが、まだまだ課題に思っていたことがありました。それは、テスト対象ファイルがCPUコア数より少ない場合、フルに並列化できないこと…。

つまり、CPUコア数が4つで、テスト対象ファイルが1つの場合、1つだけでテストして、残りの3つは何もせずに終了するのです。しかもその1ファイルのテストにかかる時間が10分とかあると長くて辛いし勿体無い!

rspecでは、テストが落ちたときに行番号指定でテスト実行できるので、それを応用してできるんじゃないかな?と思い、やってみました。

仮説の検証

rspecは行番号指定のテストを複数渡して動くか?

rspecに一度に行番号指定のファイル一覧を渡してテストができるかどうかを検証してみたところ、成功。

paralle_testsは行番号指定のテストを複数渡して動くか?

次に、parallel_testsに対して一度に行番号指定のファイル一覧を渡してテストができるかどうかを検証してみたところ、失敗…。これは調べてみたところ、parallel_testsのテストケースの分割方法が、デフォルトでファイルサイズのため、ファイルの有無のチェックを行なっているためでした。--group-byオプションをfoundに指定したところ、成功!これでとりあえずparallel_testsでテストケース毎に分散実行できることがわかったので、あとはテストケースを抽出するだけ。

実装する

テストケースを抽出する

テストが落ちたときには行番号付きで落ちたテスト一覧が表示されるので、なんかあるだろうと考えていたところ、テスト結果の出力をjson形式にして、--dry-runオプションをつけたら、実際にはテストを実行せずにテストケースを行番号付きで取得できたので、これを加工することにした。

bin/rspec --dry-run --format json --out tmp/test-results.json spec/models/user_spec.rb

ちなみに、標準出力でjsonを取得しようとしたところ、simplecovが実行されてjson以外の文字列が混ざってしまってjsonにならなかったため、--outオプションでファイルにした。

テストケースのjsonをパースする

テストケースをparseして、テストファイル名と行数を結合すればOK、と思っていたのだけれど、it_behaves_likeを使ってテストの共通化をしているところは、テストファイル名と行番号がshared_examples_forを定義していたところになってしまっていて、ダメそうだった。以下のfile_pathline_numberのところ。

{
  "version": "3.8.0",
  "examples": [
    {
      "id": "./spec/models/user_spec.rb[1:1:3:1]",
      "description": "エラーになること",
      "full_description": "User エラーになること",
      "status": "passed",
      "file_path": "./spec/models/user_spec.rb",
      "line_number": 106,
      "run_time": 2.0e-06,
      "pending_message": null
    },
    {
      "id": "./spec/models/user_spec.rb[1:1:4:1]",
      "description": "並び順が1以下だとエラーになること",
      "full_description": "User behaves like display_order_validation 並び順が1以下だとエラーになること",
      "status": "passed",
      "file_path": "./spec/support/display_order_validation.rb",
      "line_number": 2,
      "run_time": 2.0e-06,
      "pending_message": null
    },
  ],
  # 略
}

代わりに、idがファイル名と個別にテストケースを認識するための文字だと思ったので([1:1:3:1]のやつ)、これを使ってみたところ、成功。

bin/以下にrubyスクリプトを作成

今回は、bin/parallel_rspec_each_exampleというファイルを作成した。実行権限を与えておくこと。

touch bin/parallel_rspec_each_examples
chmod +x bin/parallel_rspec_each_examples

そして、ファイルには以下の内容を記述する。

parallel_testsを使ってテストケース単位でテストを分割して実行する

使う

実際に使ってみる。

テストファイルを1つ指定して並列実行

bin/parallel_rspec_each_examples spec/models/user_spec.rb

テストファイルを複数指定して並列実行

bin/parallel_rspec_each_examples spec/models/user_spec.rb spec/models/group_spec.rb spec/models/article_spec.rb

並列数を指定して並列実行

bin/parallel_rspec_each_examples -n 2 spec/models/user_spec.rb

応用編

CircleCIで落ちたテストの一覧を取得する仕組みと組み合わせる

過去に書いた、落ちたテストの取得方法は以下の記事。

patorash.hatenablog.com

ちなみに以下のはshellがfishの場合なので、他のshellを使っている場合はxargsの指定が異なるかもしれません。

bin/rails runner script/circleci_failed_spec_files.rb -j test | xargs bin/parallel_rspec_each_examples

これで、落ちたテストが1ファイルであろうと、並列実行でテストされるのでローカルでの検証時間が短くて済みます!