minitest-parallel_fork

minitest-parallel_fork adds fork-based parallelization to Minitest. Each test/spec suite is run in one of the forks, allowing this to work correctly when using before_all/after_all/around_all hooks provided by minitest-hooks. Using separate processes via fork can significantly improve spec performance when using MRI, and can work in cases where Minitest’s default thread-based parallelism do not work, such as when tests/specs modify the constant namespace.

Installation

gem install minitest-parallel_fork

Source Code

Source code is available on GitHub at github.com/jeremyevans/minitest-parallel_fork

Usage

You can enable fork-based parallelism just by requiring minitest/parallel_fork. One easy to do so without modifying the spec code itself is to use the RUBYOPT environment variable. So if you execute your specs using:

rake spec

You can switch to fork-based parallelism using:

RUBYOPT=-rminitest/parallel_fork rake spec

To control the number of forks, you can set the NCPU environment variable:

NCPU=8 RUBYOPT=-rminitest/parallel_fork rake spec

If you don’t set the NCPU environment variable, minitest-parallel_fork will use 4 forks by default.

Hooks

In some cases, especially when using external databases, you’ll need to do some before fork or after fork setup. minitest/parallel_fork supports before_parallel_fork and after_parallel_fork hooks.

before_parallel_fork is called before any child processes are forked:

Minitest.before_parallel_fork do
  DB.disconnect
end

after_parallel_fork is called after each child process is forked, with the number of the child process, starting at 0:

Minitest.after_parallel_fork do |i|
  DB.opts[:database] += (i+1).to_s
end

The above examples show a fairly easy way to use minitest-parallel_fork with an external database when using Sequel. Before forking, all existing database connections are disconnected, and after forking, the database name is changed in each child to reference a child-specific database, so that the child processes do not share a database and are thus independent.

ActiveRecord

To use this with Rails/ActiveRecord, you probably want to use hooks similar to:

Minitest.before_parallel_fork do
  ActiveRecord::Base.connection.disconnect!
end

Minitest.after_parallel_fork do |i|
  db_config = Rails.application.config.database_configuration[Rails.env].clone
  db_config['database'] += (i+1).to_s
  ActiveRecord::Base.establish_connection(db_config)
end

Speedup

The speedup you get greatly depends on your specs. Here’s some examples using Sequel’s specs:

                   2 forks         4 forks
spec_core:      1.25x - 1.36x        1.5x   
spec_model:     1.29x - 1.62x   1.72x - 2.02x
spec_plugin:    1.57x - 1.76x   2.29x - 2.37x    
spec_sqlite:    1.75x - 1.86x   2.26x - 2.65x
spec_postgres:  1.32x - 1.40x     Untested

License

MIT

Author

Jeremy Evans <[email protected]>