test-queueでRailsアプリのRSpecの実行時間を短縮する

RailsアプリのテストをRSpecで書いていて、examplesの数が増えていくと、 RSpecの実行時間が長くなっていきます。

この実行時間を短縮する方法を調べていて、test-queueというライブラリにたどり着いたので、 使い方を纏めておきます。


test-queue

test-queueは、テストを並列実行するテストランナーで、 MITライセンスのオープンソースとして公開されている。

種々のテストフレームワークでのテストケースを並列実行するツールで、

  • testunit
  • minitest
  • rspec
  • cucumber

に対応している。

環境

試した環境は、以下の通り。

  • ruby 2.3.3
  • rails 5.0.1
  • rspec-rails 3.5.2
  • test-queue 0.4.2

導入

Railsアプリにtest-queueを導入するには、Gemfileに

group :development, :test do
  gem 'test-queue'
end

を追記し、bundle installする。

この時点で、この時点で、test-queueが提供するRSpec用のランナーであるrspec-queueが使えるようになる。

試しに、個人的に開発しているアプリの環境で実行してみた。

bundle exec rspec-queue spec

すると、

1) UsersController GET #show when an user is logged in, returns https status 200.
   Failure/Error: @user = FactoryGirl.create(:user)

   ActiveRecord::RecordInvalid:
     Validation failed: Email has already been taken, Name has already been taken
   # ./spec/controllers/users_controller_spec.rb:9:in `block (4 levels) in <top (required)>'

のようにModelのValidationでエラーが発生し、テストに失敗する。

(上記のエラーは、テストデータの投入時にModelのUniqueness Validatorにひっかかり、テストが失敗した。 もちろん、test-queueを使わない環境では、正常にパスするテストケース。)

これは、test-queueが生成する複数のワーカープロセスが、同一のデータベースにアクセスすることで発生する。 ワーカー単位で異なるデータベースを使用するようにする必要がある。

実際、test-queueのREADMEには、

Since test-queue uses fork(2) to spawn off workers, you must ensure each worker runs in an isolated environment. Use the after_fork hook with a custom runner to reset any global state.

とある。また、

But the underlying TestQueue::Runner::TestUnit, TestQueue::Runner::MiniTest and TestQueue::Runner::RSpec are built to be subclassed by your application. I recommend checking a new executable into your project using one of these superclasses.

ともある。つまり、

  • test-queueはworkerを作るのにfork(2)を使ってるので、各workerが隔離された環境で動くようにしないとだめ。
  • カスタムランナーを作って、after_forkフックでグローバルな状態をリセットしよう。
  • TestQueue::Runner::TestUnitなどは、アプリケーションでサブクラスを作って使うよう設計されてる。

のようなので、やはり、自作のランナーを作って、複数のワーカーが独立して動くようにする必要がある。

独自Runnerの実装

test-queueのワーカー単位で異なるデータベースにアクセスするための独自のランナーを次のように実装してみた。

#!/usr/bin/env ruby
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'test_queue'
require 'test_queue/runner/rspec'
class RSpecQueueRunner < TestQueue::Runner::RSpec
def after_fork(num)
ENV.update('TEST_ENV_NUMBER' => num.to_s)
ActiveRecord::Base.configurations['test']['database'] << num.to_s
ActiveRecord::Base.establish_connection(:test)
ActiveRecord::Tasks::DatabaseTasks.drop_current
ActiveRecord::Tasks::DatabaseTasks.create_current
ActiveRecord::Tasks::DatabaseTasks.load_schema_current
end
end
RSpecQueueRunner.new.execute
view raw my-rspec-queue hosted with ❤ by GitHub

これを、bin/my-rspec-queueに配置し、chmod +xする。

実行

上記のbin/my-rspec-queueを実行すると、次のような結果が得られた。

% bin/my-rspec-queue spec
Starting test-queue master (/tmp/test_queue_16384_46915722628380.sock)

==> Summary (2 workers in 11.6600s)

   [ 1]                           44 examples, 0 failures, 6 pending        13 suites in 11.6425s      (pid 16420 exit 0 )
   [ 2]                           49 examples, 0 failures, 6 pending        12 suites in 11.6322s      (pid 16423 exit 0 )

上述のエラーが解消され、2つのワーカーで並行してテストを実行できた模様。 ちなみに、ワーカー数はデフォルトでシステムのコア数になる模様。

通常のbundle exec rspecbin/my-rspec-queueで実行時間を比較してみたところ、

env time(sec)
rspec 25.668
test-queue 18.676

となった。この検証でのRSpecのexample数は、93(うち、12pendings含む)。

へっぽこマシン、かつ100弱のテストケースでも約30%ほど実行時間が短縮されているので、 より現実的な環境であれば、さらにtest-queueの能力が際立つと思われる。

参考

以下のサイトを参考にさせて頂きました。


以上、test-queueを使ってRSpecの実行時間を短縮する方法について纏めてみました。

大量のテストケースを高速マシンで処理させてみたいものです。