Fast specs - Run your specs in less than 1 second

Okay, let me clarify that title first. I, as most of you, have two sets of tests for my Rails application: rspec and cucumber. rspec heavily focusses on testing models and business logic while cucumber focusses on testing the entire application stack and user interaction.

The problem is that as your app grows, your test set grows - and so does the time it takes to run those tests.

This post is inspired by Corey Haines’ talk at Arrrrcamp (Oct 2011) and my own experience with writing fast specs.

Red - Green - Refactor

If you do TDD/BDD you most likely follow the Red-Green-Refactor pattern:

  1. Write one test, and see it fail (red)
  2. Write the most minimal implementation to satisfy that test, and see it pass (green)
  3. Refactor your code to look/perform better (refactor)
  4. Repeat

But what if your test suite takes >30 seconds to run. You write a test, then wait 30 seconds to see the test fail. You then write the simple implementation, wait 30 seconds. Oops, you made a typo - fix it, wait 30 seconds. Now it passes. Refactor, again wait 30 seconds.

I think this scenario is very familiar for many rails developers.

Take a closer look

So, let’s take a closer look at a real-world example.

This is a Post model, it belongs_to and author and it can give you a summary of an article by returning the text above a ‘~’ marker.

# app/models/post.rb
class Post < ActiveRecord::Base
  DELIMITER = "~\n"

  belongs_to :author

  def summary
    summary = if body =~ /#{DELIMITER}/i
      body.split(/#{DELIMITER}/i).first.strip
    else
      body
    end
  end
end

This is a spec that’s defined for the Post model:

# spec/models/post_spec.rb
require 'spec/helper'

describe Post do
  context "summary" do
    it "should return the summary" do
      post = FactoryGirl.build(:post, :body => "Summary\n~\nNo summary.")
      post.summary.should == "Summary"
    end
  end
end

Running this spec would take at least 10 seconds. Running time rspec spec/models/post_spec.rb woudl output something like this:

Finished in 0.05523 seconds
real  0m10.387s

This means that running the actual spec took 0.05s, but running the entire command took 10 seconds. What is slowing us down?

Dependencies

As you see in the spec above I use FactoryGirl.build instead of FactoryGirl.create to prevent interacting with the database. I do this because hitting the database slows down your test.

I removed the database dependency in order for my test to run faster.

What other dependencies are there that could be removed in order to test the summary functionality?

Any idea?

Yes!

Rails.

Rails is a dependency to your app

You have a Rails-app. But Rails is not your app, it’s a dependency, just like the pg and haml gems are dependencies.

Loading the entire Rails stack takes quite some time. And just like with the database dependency we must ask ourselves: do we really need Rails to perform this test?

There are a lot of scenarios where the answer to that question is NO.

Specs without Rails

This may seem a bit weird at first, but let’s take another look at the Post model:

# app/models/post.rb
class Post < ActiveRecord::Base
  DELIMITER = "~\n"

  belongs_to :author

  def summary
    summary = if body =~ /#{DELIMITER}/i
      body.split(/#{DELIMITER}/i).first.strip
    else
      body
    end
  end
end

The summary method does not interact with the Post model at all, except that it access the body attribute. But the body attribute is just a String.

So, if we wanted to test the summary method and remove all Rails dependencies, we’d have to remove ActiveRecord.

Consider this:

# app/logic/myapp/summary.rb
module MyApp
  class Summary
    def self.for(text, delimiter)
      summary = if text =~ /#{delimiter}/i
        text.split(/#{delimiter}/i).first.strip
      else
        text
      end
    end
  end
end

I think you can quickly see that this method does exactly the same as Post#summary. But it does not have any dependency to ActiveRecord.

Could we rewrite our test for this new class?

# fast_spec/myapp/summary_spec.rb
require 'myapp/summary'

describe MyApp::Summary do
  it "should return the summary" do
    MyApp::Summary.for("Summary\n~\nNo summary.", "~\n").should == "Summary"
  end
end

That looks good! Note that this spec does not include spec_helper. spec_helper is responsible for loading up your test environment, which normally includes all your app dependencies, including Rails.

Your new Post model

The Post model should also be updated, of course to utilise this new class.

# app/models/post.rb
class Post < ActiveRecord::Base
  DELIMITER = "~\n"

  belongs_to :author

  def summary
    MyApp::Summary.for(body, DELIMITER)
  end
end

The spec for Post should also be changed. Since we already have tested that MyApp::Summary#for returns the right summary for a given text and delimiter, all we have left to do is make sure that Post#summary calls it correctly.

# spec/models/post_spec.rb
require 'spec/helper'

describe Post do
  context "summary" do
    it "should return the summary" do
      post = FactoryGirl.build(:post)
      MyApp::Summary.should_receive(:for).with(post.body, Post::DELIMITER)
      post.summary
    end
  end
end

Running fast specs

The files above are located in fast_spec and app/logic. I do this because I want to separate my fast_specs so I can run them independtly from my normal specs.

Running fast specs works like this:

rspec -I app/logic fast_spec

Try it out with time rspec -I app/logic fast_spec:

Finished in 0.03223 seconds
real  0m0.421s

That’s your same spec, down from about 10 seconds to 0.5 second.

The big picture

By now you have seen that you can extract business logic into a seperate class. You’ve also seen that you can write tests that don’t depend on Rails and run amazingly fast.

Of course you ask, how does this relate to normal RSpec and Cucumber tests?

My opinion is that your fast_specs test business logic that does not relate to anything Rails specific, like making calculations, processing text, et cetera.

When you make changes to your business logic, you can test is very quickly. This shortens your TDD cycle and allows you to focus more on the task at hand (instead of waiting for your tests).

RSpec tests are there to test integration with your dependencies like Rails. This is the place where test scopes and mailers.

Cucumber still has its place to test user interaction with your app. Can a user still click the ‘Order’ button and get the proper response from our app.

RSpec and Cucumber will now be ran less often. Maybe only before you commit code - or maybe have your CI run them for you?

Added benefit - Design

An added bonus of TDD is that your tests dictate the design of your app. By using fast_spec you force your business logic into separate classes. This makes them even more re-usable than normal Rails models.

Your ActiveRecord models now become cleaner and are more a configuration of Rails than anything else.

By doing this your app will become way more maintainable, easier to test, faster to test and your code will be more re-usable. What’s not to like?

Where to go from here?

As Corey Haines put it:

Try to extract one single method from your app into a fast_spec. Just one. When you have that one spec running so fast it’s easy to add more.

This would also be my advise to you. Try to extract one piece of functionality from a model into a fast_spec. It may be difficult at first, but try it and see the results.

Then, when you have that one example working, add more.