How we made our rspec test suite to run 2x faster

Why on earth is my test suite taking so long to run?” If you are a developer, you might have asked this question at least once to yourself. So did we, our rails project’s test suite was taking a good 1 hour 30 minutes to run and we wanted to improve that time so badly that we eventually did exactly that, reducing nearly 1 and half hours to just minutes, and this is how we did it.

1. Database independent tests

DB operations are often time-consuming and most of the time we can do away with saving objects to the database to run our tests. Use test doubles, stubs and mocks instead of creating the real instance of a class and invoking methods on the created instance.
class Student < ActiveRecord::Base
  .
  .
  def name
    first_name +" "+ last_name
  end
end
Our test case
describe Student do
  let (:student) {create(:student, first_name: 'Red', last_name: 'Panther')}
  it 'should return name' do
    student.name.should == 'Red Panther'
  end
end
This test can be made faster by replacing
let(:student) {create(:student, first_name: 'Red', last_name: 'panther')}
with
let(:student) {build_stubbed(:student, first_name: 'Red', last_name: 'Panther')

2) Use gem group

Rails preload your gems before running tests. Using gem groups allow rails to load only the environment specific dependencies.
#Gemfile
group: production do
  gem 'activemerchent'
end
group :test, : development do
  gem 'capybara'
  gem 'rspec-rails'
  gem 'byebug'
end

3) Use before(:each) and before(:all) hooks carefully

Since before(:each) runs for every test, be careful what we include inside before(:each) hook. If the code inside before(:each) is slow every single test in your file is going to be slow. A workaround would be to refactor the code to have fewer dependencies or move them to a before(:all) block which runs only once. Let’s say you have
before(:each) do
  @article = create(:article)
  @author = create(:author)
end
moving them to a before(:all) block
before(:all) do
  @article = create(:article)
  @author = create(:author)
end
Should save you some time but with some drawbacks of its own, for example, the objects @article and @author are not recreated for each test as they in before(:all) block which means any test case that changes the attributes of these objects might affect the result of other following tests.

4. Use build_stubbed Instead of build

FactoryGirl.build is not suitable when we want our instance to behave as though it is persisted. In this scenario instead of creating a real class instance, we can use build_stubbed which makes the instance to behave as it is persisted by assigning an id.
FactoryGirl.build_stubbed(:student)
Also note that when we build instance using ` .build` it calls .create on the associated models, where as .build_stubbed calls nothing but .build_stubbed also on associated models as well.

5. Running tests parallelly

parallel_tests is a gem that allows us to run tests across multiple CPU cores. A very important thing to take into account when running tests in parallel is to make sure that the tests are independent of each other. Even though parallel_tests uses one database per thread, if there are any shared state between tests that live outside the DB such as Elastic search or Apache solar those dependencies should be taken into account when writing tests.

6. Use continuous integration

As our test suite grew into 3k test cases, it was no longer viable to run the entire suite on our local machines. That’s when we felt the urgency to switch to a CI. We chose Circle CI which supports parallel builds. We split out tests into multiple virtual machines that run parallelly and it was a huge win for us in terms of test times. Our developers wrote the code and pushed to the repo and the CI took care of the rest. Few popular CI tools are 1) Trvis CI 2) Jenkins 3) CircleCI 4) Codeship Automated tests with continuous integration also enhance code quality.

7. Database cleaner

We observed an increase in speed after tweaking our database_cleaner strategies a little bit. To start with, include gem database_cleaner in gemfile. Inside a separate file spec/support/database_cleaner.rb,
RSpec.configure do |config|
 
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end
 
  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end
 
  config.before(:each) do
    DatabaseCleaner.start
  end
 
  config.after(:each) do
    DatabaseCleaner.clean
  end
 
end
  •  This will clear the test database out completely before the entire test suite runs.
  •  Sets the default database cleaning strategy to be transactions, which are very fast.
  •  DatabaseCleaner.start and DatabaseCleaner.clean hook up database_cleaner when each test begins and ends.

References

]]>