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 endOur 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 endThis 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
#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
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) endmoving them to a
before(:all)
block
before(:all) do @article = create(:article) @author = create(:author) endShould 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 filespec/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
andDatabaseCleaner.clean
hook up database_cleaner when each test begins and ends.
Thanks for the article!
Regarding parallel tests when you have multiple parallel CI nodes then you can split test suite evenly across CI nodes with knapsack gem I developed https://github.com/ArturT/knapsack/ Maybe it will be helpful for someone 🙂
There is a faster alternative(but maybe less flexible) to database_cleaner: https://github.com/amatsuda/database_rewinder.
There is a faster(but less flexible) alternative to database_cleaner: https://github.com/amatsuda/database_rewinder.
great alternative.. Thanks
Small typo: “Trvis Ci”
+ remove all callbacks in models
This part cannot work:
You will get:
It should be either `let` or `before`.
In any case `before (:all)` can be pretty dangerous. Someday you may shoot yourself in the foot.
The main idea is that all tests should not depend on the results of others.
Using `before (:all)` objects `@autor` and `@article` are not recreated for each test.
That being said, if attributes of these objects are changed in some tests, that can affect results of other tests.
:+1: 😉
Thanks Vitaly for your comment. Edited the article accordingly