Jun 30, 2017

Speeding up tests in Ruby on Rails

blogpost cover image
As everyone knows - “tests are very important!”, and “oh no! this application has no tests!” are the most common sentences in the subject of tests. Indeed tests are important, but it’s nice when they are created properly, so we don’t have to go for a coffee after running them. I would like to present to you some tips on how to increase the speed of our tests. 

Unit & Integration tests

To get fast tests, you have to think about this at the very beginning of writing them.
Try to have many unit tests and less integration tests. Keep things in a black box. If you are testing some class which uses a lot of other classes, and those other classes are of course tested as well, you are creating unnecessary time consumption when running those tests.

Unit tests

Unit testing is about testing a single and isolated piece of code. This means output from function Foo should depend only on code from this function or at least code of its class. Creating them should be fast. Sometimes you will have the need to test a private or protected method. You can bypass encapsulation by using `send` method.


However, testing private methods must be done very carefully, as common practice is that private and protected methods should not be tested directly as they do not count towards class api.
If the code is very poorly designed (i.e. whole code in one function or a few hundreds lines of code in one class), those tests will be impossible or very time consuming to write. The concept of unit tests helps keep our code compliant with single responsibility principle, which means that one function should only do one thing.

Integration tests

The role of integration tests is to combine unit tests and check if they are communicating properly. Because of their higher complexity, they are much slower than unit tests. They can be surely written much faster than unit tests, but if any test fails we know only the general area where the bug occurs.
If methods are deeply chained, it is generally a good idea to write an integration test for the whole action, just to be sure that the flow works.

Mock & Stub

Sometimes it’s hard to write unit tests, even if the code is written properly. Or maybe we want to write an integration test only for a part of whole flow. Other case is to test something around an external service which we can’t use in tests. No worries, stubs and mocks are here to help us!

If you would like to read more about mocks and stubs, I recommend the article written by Marko Anastasov, "Mocking with RSpec: Doubles and Expectations".


It’s easy to write unit tests for bottom flow methods. The problem is where the method has to use another method which we don’t want to bother with the unit test, or we have tested it before. This is a job for stub which is used to cover an behavior of single function with fixed result value.
There are a lot of stubbing options:

allow_any_instance_of(SomeClass).to receive(:foo).and_return("result")

This will stub foo method in all SomeClass objects, so it will always return “result”.

allow(some_object).to receive(:foo) { “result” }

This is going to cover method foo only in declared object.

allow(some_object).to receive(:multiple_string).with(1, “text”) { raise "this error" }

If we have multiple uses of a single method and we expect different results, we can use `with` to stub a method which only gets specific attributes. We can also raise an exception as a return value.
This is a very useful tool - we can stub unnecessary methods to give a huge speed boost to our tests.


This is a more complex tool. Like stub was for a single method, mock is responsible for faking the whole object behavior. Sometimes some method or gem connecting to api, may respond with an object. We should mock this. In rspec there is a class Rspec::Mocks::Double which allows us to do so. Don’t confuse mocks double with double as datatype. 

company = double(name: “BinarApps”, class: Company)

This line will create us a company object with callable “name” with the result of “BinarApps”. We can also call the "class" method to get proper Company class type, but do it only if you really need this. When we override class type like that, it can be harder to see what is going on with our tests.

Multithread parallel_tests

If you have a lot of tests, it may be annoying that they are running for few minutes, maybe even dozens of minutes or more (hope not!).
The solution for this is to use the Parallel Tests gem. https://github.com/grosser/parallel_tests
Generally speaking, it runs tests on multiple processors to make it faster. The readme is written very clearly, so I won’t reduplicate the installation guide. Just remember to create those multiple databases, so every test process has its separate database. Anyway, it will work more or less without them, but it is very important to have cleaned the database before each test. The gem divides the tests by a number of lines by default, but we can change this to divide by run time.
This gem has multiple integrations for tools like capybara. Thanks to this, it opens a web browser for each test process, efficiently increasing speed.
Unfortunately, this solution is not for every application. It may have trouble with some integrations like elasticsearch. The longer the specs are, the higher performance boost you will notice after adding this gem. I wouldn’t recommend this for few seconds tests.

Rspec profile

If you want to check which tests need speed improvement, an easy way is to use a profile parameter for rspec.

rspec --profile

This command runs specs and shows you 10 slowest examples. Thanks to that, we can find the black sheep and eliminate the issue.

It would be a perfect world if everyone in their projects would obey those rules, and what is most important - create tests. Write them even for simple methods. You can think “it’s a simple method, what can go wrong here?”, but later someone may unconsciously change something, and tests will detect inconsistencies and show their exact place.
So remember “tests are very important!”, and write them properly, so no one would complain about this later!