RSpec Best Practices

I’ve been using RSpec on and off for the past 2 years. I’ve learnt that it’s easy to write tests that are bloated, slow and don’t offer any value. I’ve also learnt a few tricks and best practices that will make your RSpec life a bit easier.

Some of these might be straightforward while others might be debatable – the only common theme is that they’re all my opinion.

1. Describe your Methods

Unit tests usually involve testing a single method on a single class. It is therefore very important that we describe the method we are testing in a consistent fashion. The Ruby documentation already sets a standard here – use . when referring to a class method and # when referring to an instance method.

describe '.build' do
describe '#admin?' do

This may seem like a simple thing, but it’s great for readability.

2. Use subject

When I started using RSpec I used to write tests like this:

describe Session do
  describe '.locate'
    before(:each) { @session = Session.locate }
  
    it 'should have a user' do
      @session.user.should_not be_nil
    end
    
    it 'should have an expiration' do
      @session.expiration.should_not be_nil
    end
  end
end

This certainly works, but it’s very verbose – especially since we’re only really testing 2 properties. Then I discovered subject, which makes this much cleaner.

describe Session do
  describe '.locate'
    subject { Session.locate }
  
    its(:user) { should_not be_nil }
    its(:expiration) { should_not be_nil }
  end
end

Do keep in mind that subject can make your code cleaner, but you don’t need to use it everywhere. Sometimes regular it blocks are still the best approach. You especially shouldn’t be calling subject explicitly.

There is also the concept of an implicit subject – basically if you don’t specify any subject the subject will be an instance of whatever is specified in the describe block (instantiated with the default constructor). This is very useful if you want to test some defaults on the object under test.

describe User do
  it { should be_valid }
end

3. Use let and let!

describe Product do
  describe '.on_sale' do
    before do
      @product_on_sale = create(:product, on_sale: true)
      @product_not_on_sale = create(:product, on_sale: false)
    end   
    subject { Product.on_sale }
    
    it { should_include @product_on_sale }
    it { should_not_include @product_not_on_sale }
  end
end

The problem with @instance variables is that they spring into existence whenever they are referenced. So if we accidentally type @prodect instead of @product this will simply created a nil reference which can give us false positives.

A better approach is to use let:

describe Product do
  describe '.on_sale' do
    let(:product_on_sale) { create(:product, on_sale: true) }
    let(:product_not_on_sale) { create(:product, on_sale: false) }
    subject { Product.on_sale }
    
    it { should_include product_on_sale }
    it { should_not_include product_not_on_sale }
  end
end

Now we will get an error if we mistype the variable name. Let also has other advantages:

  • It will return the same reference when use multiple times in the same example, but not across examples
  • It’s lazy loaded so you won’t waste time initializing variables that you don’t need

Of course, having variables lazy loaded is not always ideal – if you want to force variables to be created you can use the alternative let! version.

4. Use specify

There are some scenarios where the it syntax really becomes a burden – the assertion is very easy to read, but it’s difficult to write a description that doesn’t just repeat the assertion. You could obviously leave the description out, but that doesn’t read any better.

describe Product do
  describe 'we can only have one featured product' do
    let(:previous_featured_product) { create(:product, featured: true) }
    let(:new_featured_product) { create(:product, featured: false) }
    before do
      new_featured_product.featured = true
      new_featured_product.save
    end
    
    it "the featured product should be updated" do
      Product.featured.should == new_featured_product
    end
    
    it "the old featured product should no longer be featured" do
      previous_featured_product.should_not be_featured
    end
  end
end

A better approach is to use specify – which is simply an alias to it, but can really help readability.

describe Product do
  describe 'we can only have one featured product' do
    let(:previous_featured_product) { create(:product, featured: true) }
    let(:new_featured_product) { create(:product, featured: false) }
    before do
      new_featured_product.featured = true
      new_featured_product.save
    end
    
    specify { Product.featured.should == new_featured_product }
    specify { previous_featured_product.should_not be_featured }
  end
end

5. Use context

As the name implies, context is simply another block (similar to describe) which helps you to organize your tests into logical blocks and improve readability. You can really create some nicely structured tests if you take advantage of the way let and subject blocks interact.

describe Product do
  describe '#on_sale?'
    subject { build(:product, original_price: 105, price: current_price) }
    
    context 'the current price is equal to the original price' do
      let(:current_price) { 105 }
      it { should_not be_on_sale }
    end
    
    context 'the current price is less than the original price' do
      let(:current_price) { 95 }
      it { should be_on_sale }
    end
  end
end

6. Use factories

This is the first ‘best practice’ which is highly debatable. Steve Klabnik wrote an excellent article on why factories can be a bad idea and how it can slow down your test suite. It might not seem like a big deal when you only have 200 or 300 tests and your entire suite runs in 30 seconds, but once you get to a point where your entire test suite takes 10 or 15 minutes to run it gets incredibly painful.

So keeping that in mind, factories can still be useful for initializing default objects. For example, you might want to test something on your OrderService class. An order is always associated with a product, so you need to create a valid product.

describe OrderService do
  describe '.order_for_product' do
    let(:product) { Product.new(sku: '123456') }
    subject { OrderService.order_for_product(product) }
    it { should be_valid }
    its(:product) { should == product }
  end
end

Great, we’re testing our service method without any need for a factory (or hitting the database). However, a month later our Product class changes slightly – we now require all products to have a price. We write a failing test for the Product validation, we add the validation to the Product class, run our test suite and… 27 failures. Uh-oh, everywhere that we’re instantiating Product in our tests needs to change – we need to add a price everywhere.

This is the kind of scenario were I find factories very useful. You can avoid factories if you really want, but I think you’re opening yourself up to a large amount of pain.

I do agree with Steve that the speed of your test suite is important and hitting the database should be avoided, but of course you can always hit the database without factories as well. Bottom line: use factories, but with caution.

7. Use matchers

RSpec has a pretty complete set of matchers which improves the readability of your code as well as improving the error message for failures.

describe Array do
  describe 'with 3 elements' do
    subject(:letters) { ['a','b','c'] }
    specify { letters.include?('d').should == true }
    specify { letters.should include('e') }
  end
end

Here are the results of these 2 failures:

1) Array with 3 elements should == true
   Failure/Error: specify { letters.include?('d').should == true }
     expected: true
          got: false (using ==)
          
2) Array with 3 elements should include "e"
   Failure/Error: specify { letters.should include('e') }
     expected ["a", "b", "c"] to include "e"          

It’s pretty easy to see which of these error messages are more readable. Any boolean property can be used as a matcher, which is why you can write code like this:

describe Product do
  describe '#featured' do
    subject { build(:product, featured: true) }
    it { should be_featured }
  end
end

Many libraries include additional matchers and you can even write your own custom matchers.

8. Use shared examples

Shared examples is a feature which is very useful for removing duplication between tests. I have found this to especially useful when I have two models with similar functionality. (Example from RelishApp documentation)

require "set"

shared_examples "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      collection.size.should eq(3)
    end
  end

  describe "#include?" do
    context "with an an item that is in the collection" do
      it "returns true" do
        collection.include?(7).should be_true
      end
    end

    context "with an an item that is not in the collection" do
      it "returns false" do
        collection.include?(9).should be_false
      end
    end
  end
end

describe Array do
  it_behaves_like "a collection"
end

describe Set do
  it_behaves_like "a collection"
end

You can also pass parameters to the shared_examples block which allows you very strong flexibility in implementing shared specs.

9. Use expect

The expect syntax was introduced in RSpec 2.11 and is very useful in scenarios where you are not using the subject syntax.

describe Product do
  describe 'the default product'
    subject(:product) { Product.new }
  
    it 'should not be on sale' do
      expect(product).not_to be_on_sale
    end
  end
end

Again, this is another tool to improve the readability of your code. The choice between expect and should simply boils down to readability.

Additional Resources

If you are completely new to RSpec or Rails there are two books which I can recommend.

The RSpec Book

Agile Web Development with Rails 4

There are obviously many other patterns which can be considered ‘best practices’ within testing – how much to test, how much to mock/stub, etc – but these aren’t really specific to RSpec, which is what I’ve tried to focus on here.

Happy coding.