Quantcast
Channel: Anatoly Spektor – My Programming Blog
Viewing all articles
Browse latest Browse all 21

10 ways how to speed your unit tests [Rails, RSpec, Capybara]

$
0
0

#10 Avoid writing to the database

Lets take simple example.

I have this test:

require 'rails_helper'

describe Address do
  let (:address) {create(:address)}
  it 'should be valid' do
    expect(address).to be_valid
  end
end

I am testing if Address is valid by passing a Factory with dummy data.
When I run this test in console I get this:

Finished in 0.02277 seconds

0.022777 For testing a single test is quit expensive, considering that I don’t have any validation on it. Why it is slow? It is slow because it writes to the database!

How to fix it ? Don’t write to DB when you don’t have to!

Lets fix this example and change 

let (:address) {create(:address)}

to

let (:address) {build_stubbed(:address)}

When we replace create with build_stubbed by building address all associations are created, but model is not saved to the database.

Want to learn more about build_stubbed? There is an amazing blog post by Josh Clayton about using build_stabbed for faster tests.

Lets run RSpec again and now we see:

Finished in 0.01242 seconds

TWICE FASTER! All we did – we did not write to database.

Note: There are cases when you need to test if data is saved into a database (e.g testing object uniqueness), in this case writing to db would make sense.

However, often, we save into database when we really don’t have to.In this case we should ask ourselves – is it really a unit test? Or is it an integration test that tests integration of app and database ? Confused ? Keep Reading!

#9 Run tests wiser

When you application grows, inevitably you will find that your test suite will become slow and optimization will not make much of a difference.

Why bigger test suites are slow ?

Because if we want to have a test suite  that is complete, we will also include integration (feature) tests and no matter what you do – integration tests will be slow.

Why integration tests are always slow?

Because they test integration of the components.  Often integration tests are actually browsing your website, so they are dependant on JavaScript load, on network connection etc.

When I say slow – I mean test suite can be 10 minutes slow!

Running tests for 10 minutes is very inefficient. Main goal of running a test suite is to make sure that the new code does not break the existing code. So if test suite runs for 10 minutes every time – developers probably won’t run  test suite at all.

What to do when big test suite is very slow?

Note: For this solution to work you need to have a strong Unit Test coverage (at least %70).

I have a hack that I have been using for some time now. It does not really solve the problem, but it makes process more efficient.

What you want to do is to run integration tests only when you integrate.

How will that work in real world?

Team of 5 people are working on different branches. While they are working – they run unit tests which are quick and make sure they all pass. Whenever fix/feature is ready it is merged into integration branch – this auto triggers CI to run both unit tests and integration tests (sort of smoke testing). Thus you can work efficiently on your feature branch, and you are covered with smoke testing when you integrate.

At this point you are probably screaming – SHOW ME THE CODE!!!!!

It can look something like this:

require 'rspec/core/rake_task'

namespace :spec do
RSpec::Core::RakeTask.new(:unit) do |t|
t.pattern = Dir['spec/*/**/*_spec.rb'].reject{ |f| f['/integration'] }
end

RSpec::Core::RakeTask.new(:integration) do |t|
t.pattern = 'spec/integration/**/*_spec.rb'
end
end

To run integration tests do:

rake rspec:integration

To run unit tests do:

rake rspec:unit

To make sure it works nicely  move all your specs that deal with database, email and other slow tests to ./spec/integration.

You might ask: When you say slow tests, how do I know which tests are slow ?

This brings us to the next point…

#8 Profile Specs and  Optimize slowest tests

It is important to know which tests are slow. RSpec provides a feature to profile your test suite and show you 10 slower running tests.

To make it work just go to .rspec file and add –profile . Next time you run your test suite, RSpec will output top 10 slowest tests in your  suite.

On the same note – it is crucially important to follow your code coverage. SimpleCov gem will help you with that.

#7 Use Ruby 2.1 and up

Not much to talk about here. Upgrade Ruby to 2.1 because it is has better memory management. Some say it is up to %15-%20 faster. So if you want to boost your test speed  this could be very beneficial.

More on update to Ruby 2.1 in this awesome post by Justin Weiss

#6 Mock External API’s

What are API requests ?

Examples of API requests : you send a request to twitter to get followers, you query google map for coordinates, you interact with your internal services via API or anything else that makes a network request and waits to get data in return.

Lets get it straight – API calls are slow! They depend on your network connection, on receiver etc. They need to be mocked!

To achieve that in Rails, I have been using webmock gem. The beauty of this gem is that when it is setup, it disables all external calls by default and throws an error if you try to connect to external API. This can be very useful in identifying external places in your code where you actually do real api calls.

#5 With Database Cleaner – use :transaction where possible

I think Avdi Grimm explained this issue best in his blog post on configuring database_cleaner . I highly recommend you to read it. Here is a quick recap: tests that have to deal with database need a way to cleanup dummy data , for that reason devs often use database cleaner gem. Database cleaner is very smart and uses multiple strategies to take care of wiping out data. Two strategies of database cleaner are transaction and truncation.

Truncationwipes all the data after each test.

Transaction – keeps data in transaction and just rollback instead of wiping out. Since rollback is much faster than truncate it is preferred to use transaction strategy. Note there are issues with transaction strategy in certain cases, however it should be a go to strategy if you want your tests to be faster!

#4 Use :js => true only if you have to

Sometimes, your integration tests need to access an element that is set by JavaScript. To make it work you have to pass :js => true to your test. Note that passing :js => true forces your test to wait for all the JavaScript to be loaded, this can take a very long time and make your test really slow. Remove :js => true anywhere when you don’t need it and it will speed up your test suite!

#3   Use before(:each) hook wisely

Often when writing tests we tend to put a lot of stuff in before(:each) hook. The main issue with that is that before(:each) runs after every test where it is included. So if your before(:each) code is slow, it will slow down every single test. Solution to that would be to migrate some of the none repeatable code to before(:all) which runs once, or even better, refactor your code, so that it has less dependencies.

#2 Don’t let Capybara Wait for too long

There are 2 parts to this point.

Point 1: If you are using sleep() in your tests – you are doing something wrong.

By design in capybara expect() syntax waits for element to be loaded. You can even configure default wait time. When you say sleep(5), it means that it will wait 5 seconds no matter how fast page loads. Whereas if you use for example expect(page.find(…*).to syntax, it will wait for less then 5 seconds if it finds element faster. So please don’t use sleep!

Point 2:  Use Capybara has_no_* instead of RSpec negatives

This is something new to me, but I found it to be very productive. I first read about it in cool blog post by Nick Gauthier .

Essential if you have for example:

expect(page).not_to have_content('Dashboard')

this code will wait until page has (or  for default wait time) an element “Dashboard”, which it never will, so it will be slow.

Whereas what you want to do is:

expect(page.has_no_content?('Dashboard')).to eq(true)

This code will look for “Dashboard” and quite if it doesn’t find it – which is much faster.

If you are curious about more of these things, I, once again, highly recommend to read this blog post.

#1 Decouple your code

Look at your code again and answer the following questions:

  • Can I extract parts of this code into a separate gem ?

If you answer yes – do it! Separate re-usable parts of your logic into a gem, test it thoroughly and save time, by not having to test it again in your test suite.

  • Can I extract logic into separate modules ?

Separating into modules gives you ability to test parts separately, this is faster! Also you can stub those modules whenever you don’t need to test them. Good example of separating into modules can be found here.

 

Have questions ? Ask them in comments! Want to see more cool stuff? Write your suggestions also in comments!

More amazing resources on the topic:

Hope this helps you to write speedy RSpecs!

Cheers,

Anatoly


Tagged: 10 ways to speed up unit tests, capybara, factoryGirl, fast rspec, mock, Rails, Rspec, Ruby, speed up unit tests, speedy rspec, stub test, unit test

Viewing all articles
Browse latest Browse all 21

Latest Images

Trending Articles



Latest Images