タイトルの通り、CirclCIで回しているテストを40%高速化した話をします。
うちの会社では、342files, 27300examples強を回しており、テスト時間が肥大化傾向にありました。
そこで、テスト高速化を図ろうと試行錯誤したので、その過程を書きます。
※RRRSpec使えよ!というツッコミはなしで。CircleCI上で試行錯誤の記録を残すために書きます。
また、spec自体の高速化ではなく、CircleCIの仕様に合わせた高速化の方法についてのみを書きます。
やり方
なんと、この2つだけです!
シンプル!なんてシンプル!
並列実行して、遅いテストを特定するだけです。
そもそも技術力なんていりません。気合と根性*1で速くできます。
並列実行する
並列数を変更
CircleCIで並列実行数を増やすオプションがあります。
Project Settings => Tweaks => Adjust Parallelism
から設定ができます。
並列数増やせば(金で解決すれば)高速化できるんじゃね?っと、頭がよぎりましたが、
今回の話は、並列数は据え置きのまま(6並列)という縛りプレイでいきます。
もちろん、並列数は多いほど高速になります。
並列で実行するときに理解しておきたいこと
並列で実行する際、CircleCI上では複数コンテナを使ってテストを実行します。
理解しておきたいのは、複数コンテナ上実行されるテストはファイル単位となることです。
example単位ではないので、注意が必要です。
また、並列化した場合、テスト完了は、最も遅く終わったコンテナに引っ張られます。
- コンテナA: 10分
- コンテナB: 20分
この場合、CircleCI上では、テスト完了時間が20分になります。
要は、1コンテナあたりの実行時間限りなく、全コンテナの平均時間に近づけたいのです。
で、分散させる方法は下記の通りです。
1. まずはcircle.ymlを修正
この設定では、テスト実行を上書きしており、parallel: true (並列実行モード)にしています。
test: override: - ./script/parallel_for_circle.sh: parallel: true
では、parallel_for_circle.shを見ていきましょう。
2. 各コンテナで実行するテストは指定する方法
#!/bin/bash set -xe i=0 files=() for file in $(find ./spec -name "*_spec.rb" | sort) do if [ $(($i % $CIRCLE_NODE_TOTAL)) -eq $CIRCLE_NODE_INDEX ];then files+=" $file" fi i=$((i+1)) done echo $files bundle exec parallel_rspec ${files[@]} -n 2 exit $?
CircleCIでは、並列実行時、各コンテナにCIRCLE_NODE_INDEX
という、何個目のコンテナか?というインデックスが振られます。
また、CIRCLE_NODE_TOTAL
全部で何並列か?という数値も取れます。
CircleCIではテストコマンドを上書きすることができるので、1ファイルごとにループを回し、
どのコンテナに割り当てるかをゴリゴリ書いていけば、均等に割り当てることができます。
もちろん、*1_spec.rb
などとすれば、コンテナを指定することができます。
が、全実行時にコンテナを指定するメリットがあまりないので、今回は上記のスクリプトで並列化します。*2
遅いテストファイルを特定する
次のステップは遅いファイルを特定して、そのファイルを分割することです。
6並列でテストを回すとこのような結果になります。
見にくくて申し訳ないですが、実行時間は
- 1台目:14:03
- 2台目:15:55
- 3台目:16:29
- 4台目:38:46
- 5台目:13:13
- 6台目:12:06
合計96分。平均16分で終わるはずなので、それ以上かかっているコンテナに遅いファイルが存在する可能性が高いです。
この中ではダントツで4台目が遅いので、4台目に時間がかかっているテストファイルが存在するだろうと推測できます。
次に、並列数はそのままで、4台目の実行されたテストファイルを実行していきます。
さきほどのスクリプトではecho $files
と書いているので、CircleCI上のダッシュボードから実行されているテストファイルが確認できますのでメモっておきましょう。
メモっておいたテストファイルの絞り込みは、単純に
rm -rf ./spec/* cat output_file | awk '{print "git checkout "$1""}' ....
などとしていけば4台目で実行されたものだけが残り、
実行することができます*3
理論的には6分ほどで終わるでしょう。
これを繰り返していけば、実行時間が長いファイルが特定できます。
遅いテストファイルを分割する
遅いファイルが特定できたら、あとはファイルを分割するだけです。
おそらく遅いファイルでは複数のdescribe
ブロックやcotext
ブロックが存在すると思います。
# coding: utf-8 require 'spec_helper' describe 'Hoge.fuga?' do let(:hoge) { FactoryGirl.create(:hoge)} subject{ hoge.fuga? } it{should be_true} end describe 'Hoge.fugafuga?' do let(:hoge) { FactoryGirl.create(:hoge)} subject{ hoge.fugafuga? } it{should be_false} end
例えば、このテストファイルが実行完了まで10分かかっていたとします。
このファイルをhogehoge_fuga_rspec.rbとhogehoge_fugafuga_rspec.rbの2ファイルに分割した場合
# coding: utf-8 require 'spec_helper' describe 'Hoge.fuga?' do let(:hoge) { FactoryGirl.create(:hoge)} subject{ hoge.fuga? } it{should be_true} end
# coding: utf-8 require 'spec_helper' describe 'Hoge.fugafuga?' do let(:hoge) { FactoryGirl.create(:hoge)} subject{ hoge.fugafuga? } it{should be_false} end
並列で実行すると、最高で5分に短縮できます。
このような泥臭いことを続けていきます。
ファイルを特定 => ファイル分割 => ファイルを特定 => ファイル分割 => ....................
ただし、分割するのはいいのだけれど、間違った分割の仕方をしたら、死罪に値するかもしれません。
たとえば、
hoge_controller_get_action_spec.rb
hoge_controller_post_action_spec.rb
とかだったら中身を見なくとも、どのようなテストが書かれているか予測ができると思うのですが、
hoge_controller_1_spec.rb
hoge_controller_2_spec.rb
などとした場合、いちいちファイルを開かねばならず、苦労しそうですので気をつけましょう。
まとめ
テクニカルなことは一つもやっていないのですが、簡単なことでテスト時間を40%も高速化することができました。*4
また、CircleCIの挙動に合わせた高速化ではなく、spec自体の高速化は、
下記のエントリが参考になりますので、お試しください。