To test fast or slow

I’m a big fan of Behavior Driven Development (BDD) and thus I’m always searching for ways to improve my testing style and toolbox.

These days it seems like everyone is looking for faster tests and for good reason since loading the entire Rails framework on every test run makes the process way to slow.

I want to address the issue of testing controllers in this article as I find it to be a confusing topic even for some of the more senior rails developers.

Who cares about slow vs. fast tests?

Well, I do. And so should you.

I’m one of those folks who like having a very tight feedback loop going when I write code — so expect the rest of the article to be somewhat biased.

The reason I like writing code test first improves my code’s design and it keeps a lot of regression bugs out.

The fast feedback loops becomes important when you write code in small chunks and test it at the same time (i.e. you make a small change like one assignment and you re-run your test; if that passes you move on to the next change and so on) because you don’t want to wait a few seconds each time you make a change.

Preloaders

The simple way of improving test speed is to use a preloader like spring, spork or zeus to pre-load and/or fork the framework so it’s not reloaded every time you run a test.

Unfortunately, every preloader (except spring) that I’ve used in the past, at some point made me loose crazy amounts of time debugging some reloading issue (e.g. the test and implementation was ok but some dependency wasn’t reloaded by the preloader).

So, as nice as those tools might look like at first, they are just hiding the problem under the rug, they’re not solving it.

Isolation style

The way you would test controllers in isolation is… you stub out calls to any collaborators and you create expectations on the methods being called. The purpose of that is to only test what the controller does and nothing else — no records being created, or mail getting sent, etc.

We are assuming that the collaborators have their own tests for the functionality they provide. So for example, if you have a MyMailer.send_email method you assume that the method does what it says it does and that it’s got it’s own test suite.

Getting out of sync

One of the major issues with this method is the fact that the stubbed APIs could get out of sync with the real APIs (i.e. the real object might rename it’s methods) and the tests would still pass but the implementation code would break. Obviously that’s something we want to avoid as much as possible.

Verifying doubles

Verifying doubles are a new feature added in RSpec 3 that will help a little with the issue described above. I’m saying a little because they don’t play nice with dynamic methods which are heavily used in ActiveRecord objects.

What verifying doubles will do is they will yell at you when your stubbed methods don’t exist on the real object. That way you will know if the API changed so you can update your tests.

Let’s see an example of isolated controller test

Let’s see the implementation first as this isn’t going to change and it will make it clearer what we’re actually testing.

class MyController < ApplicationController
  def my_action
    User.create(:email => "jdoe@example.com",
                :password => "secret",
                :password_confirmation => "secret")
  end
end

So here’s what the test looks like:

require 'rails_helper'

RSpec.describe MyController, :type => :controller do
  describe "#my_action" do
    it "creates a user" do
      expect(User).to receive(:create)
      get :my_action
    end
  end
end
Finished in 0.03137 seconds (files took 0.26216 seconds to load)

Pros and cons

Pros:

  • Very fast (under a second)
  • Isolated (they correctly identify the failing code)
  • Improve design (they force you to move logic out of the controller)

Cons:

  • Brittle (APIs can get out of sync)
  • Harder to maintain (you need to update the test more often)

Integration style

This style of testing is making assumptions about the end result and it also touches more than just the controller, it tests the database as well. One of the nice things about this method is you don’t really care how the controller does what it does and what methods it calls, you can test it like a black box.

There are two important issues worth mentioning:

  1. Because the tests create records in the database, they become slower, and thus the whole suite becomes slower. One workaround is to use FactoryGirl’s .build_stubbed and .build methods so you can stub as much as possible but that will only get you so far.

  2. The second annoying issue is that when something breaks in say the model, your controller tests could also break even though the controller code wasn’t touched at all. Now, I don’t know about you but I don’t like that, I don’t like hunting thru controller tests only to realise there’s nothing wrong with them.

Let’s see an example of integration style test

The implementation code doesn’t change, we’re only changing the testing style. So here’s how the test looks like:

require 'rails_helper'

RSpec.describe MyController, :type => :controller do
  describe "#my_action" do
    it "creates a user" do
      expect {
        get :my_action
      }.to change(User, :count).by(1)
    end
  end
end
Finished in 0.39168 seconds (files took 0.25017 seconds to load)

Pros and cons

Pros:

  • Don’t care about the implementation
  • Less brittle

Cons:

  • Slower because they creates records in the db
  • Tests can fail even if the controller code is not broken
  • Test more than their own responsibility

Let’s see a better example

Of course with just one test, the difference in speed doesn’t seem to be a big deal. But let’s change the number of records it creates and see if that changes anything.

RSpec.describe SiteController, :type => :controller do
  describe "#my_action" do
    it "creates a user" do
      expect {
        get :my_action
      }.to change(User, :count).by(100)
    end
  end
end
class SiteController < ApplicationController
  def my_action
    100.times do |idx|
      User.create(:email => "jdoe#{idx}@example.com",
                  :password => "secret",
                  :password_confirmation => "secret")
    end
  end
end
Finished in 2.82 seconds (files took 0.24891 seconds to load)
require 'rails_helper'

RSpec.describe SiteController, :type => :controller do
  describe "#my_action" do
    it "creates a user" do
      expect(User).to receive(:create).exactly(100).times
      get :my_action
    end
  end
end
Finished in 0.03458 seconds (files took 0.24967 seconds to load)

So here, the difference is huge (from 0.034s to 2.82s) and these are just 100 records — which is not a lot. Your app will probably grow if it’s any good and you’ll end up creating thousands of records in the database and those will quickly add up and bring your test suite to a crawl. Just trust me on this if you’ve never felt the pain before.

Even the two second delay is unacceptable in my book since it breaks your feedback loop. You don’t want to wait two seconds, you want it to be fast, instantly fast.

Conclusion

I hope I’ve given you enough information on the pros and cons of each method so you can choose which one works best for you. I’d also love to hear your comments.

I highly recommend this awesome talk by Sandy Metz, It’s a must watch.