There comes a time in the life of every Ruby on Rails project where you and your team will end up with long running tests. Rails tests can contain factories, fixtures and lots of setup procedures before tests are executed, and all of these can contribute to major slowness in the test runs.
I’ve seen four ways of dealing with slow running tests in Rails:
- start deleting tests (or mark them as skip-able): the drawback is your test coverage drops and you may end up with more bugs
- use more integration tests rather than unit tests: this exercises a lot of code paths so test coverage won’t drop too much but in the end most developers will write integration tests that exercise the “happy path”
- Run your tests in parallel
- Run only the tests that were most likely to be affected by recent code changes
The latter two methods are much better than the first two.
My favourite so far is #3 because it still runs all of the integration and unit tests that you have but makes use of the fact that you can offload the work of running tests to multiple machines. Everyone now has access to multiple machines through AWS (Amazon Web Services) or Microsoft Azure or RedHat’s cloud. You can even run tests in parallel on your local development machine.
Run Your Ruby on Rails Tests In Parallel
- Parallel_tests is a Ruby gem for Ruby on Rails that lets you run tests in parallel.
- How to set up parallel tests in RSpec across multiple machines using Jenkins CI.
The idea is that each test file will be run in separate processes.
If you have 5 tests that have a duration 2 minutes, in sequential runs it will take 10 minutes to run the whole test suite. If you have 5 processes available for parallel runs, it will only take 2 minutes to run the whole test suite.
That’s a huge difference and means you can make 30 test runs in an hour rather than 6 test runs.
Splitting RSpec test files into multiple files
However, some of your Rails tests may take longer; within the spec you could have multiple test cases and contexts that are taking too long. For example, one test context with a few tests could be taking 1 minute while the rest of the test cases in that file only take 10 seconds. At that point, you can split the long-running test context into multiple files.
Here’s an example of how that might look like:
# spec/my_controller_spec.rb describe MyController do context 'Slow tests' do it 'runs slowly #1' do # ... end it 'runs slowly #2' do # ... end end context 'Faster part of the test suite' do it 'runs in 10 seconds or less' do # ... end end end
And now here’s how we could split that controller rspec test file into multiple files.
# spec/my_controller_spec.rb describe MyController do context 'Faster part of the test suite' do it 'runs in 10 seconds or less' do # ... end end end # spec/my_controller_slow_1_spec.rb describe :MyControllerSlow1 do def self.described_class MyController end include_context 'my controller helpers' it 'runs slowly #1' do # ... end end # spec/my_controller_slow_2_spec.rb describe :MyControllerSlow2 do def self.described_class MyController end include_context 'my controller helpers' it 'runs slowly #2' do # ... end end
The reason this works is because we override the class method
described_class. This method is used by the rspec-rails extensions that make it easier to test Rails classes with RSpec.
By using shared contexts and helpers and by splitting files, you can optimize your Rails tests even further.
(If you liked this article, I have written about using mocks with Django and Python unit and integration tests and using Protractor with AngularJS and how you can take screenshots with Selenium during test runs.)