This is the second post in the series about antipatterns in testing Rails applications. See part 1 for thoughts related to fixtures and factories.

Creating records when building would also work

It is common sense to say that with plain ActiveRecord or a factory library you can both create and only instantiate new records. However, probably because testing models so often needs records to be present in the database, I have seen myself and others sometimes forget the build-only option. It is however one of the key ways to making your test suite faster.

Consider this spec for full_name:

describe User do
  describe "#full_name" do

    before do
      @user = User.create(:first_name => "Johnny", :last_name => "Bravo")
    end

    it "is composed of first and last name" do
      @user.full_name.should eql("Johnny Bravo")
    end
  end
end

It triggers an interaction with database and disk drive to test logic which does not require it at all. Using User.new would suffice. Multiply that by the number of examples reusing that before block and all similar occurrences and you get a lot of overhead.

All model, no unit tests

MVC plus services is good enough for typical Rails apps when they’re starting out. Over time however the application can benefit from some domain specific classes and modules. Having all logic in models ties it to the database, eventually leading to tests so slow that you avoid running them.

If your experience with testing started with Rails, you may be associating unit tests with Rails models. However in “classical” testing terminology, unit tests are considered to be mostly tests of simple, “plain-old” objects. As described by the test pyramid metaphore, unit tests should be by far the most frequent and the fastest to run.

Not having any unit tests in a large application is not technically an antipattern in testing, but an indicator of a possible architectural problem.

Take for example a simple shopping cart model:

class ShoppingCart < ActiveRecord::Base
  has_many :cart_items

  def total_discount
    cart_items.collect(&:discount).sum
  end
end

The test would go something like this:

require "spec_helper"

describe ShoppingCart do

  describe "total_discount" do

    before do
      @shopping_cart = ShoppingCart.create

      @shopping_cart.cart_items.create(:discount => 10)
      @shopping_cart.cart_items.create(:discount => 20)
    end

    it "returns the sum of all items' discounts" do
      @shopping_cart.total_discount.should eql(30)
    end
  end
end

Observe how the logic of ShoppingCart#total_discount actually doesn’t care where cart_items came from. They just need to be present and have a discount attribute.

We can refactor it to a module and put it in lib/. That would be a good first pass — merely moving a piece of code to a new file has benefits. I personally prefer to use modules only when I see behavior which will be shared by more than one class. So let’s try composition. Note that in real life you usually have more methods that can be grouped together and placed in a more sophisticated directory structure.

# lib/shopping_cart_calculator.rb
class ShoppingCartCalculator

  def initialize(cart)
    @cart = cart
  end

  def total_discount
    @cart.cart_items.collect(&:discount).inject(:+)
  end
end

To test the logic defined in ShoppingCartCalculator, we now no longer need to work with the database, or even Rails at all:

require 'shopping_cart_calculator'

describe ShoppingCartCalculator do

  describe "#total_discount" do

    before do
      @cart = double
      cart_items = [stub(:discount => 10),
                    stub(:discount => 20)]
      @cart.stub(:cart_items) { cart_items }

      @calculator = ShoppingCartCalculator.new(@cart)
    end

    it "returns the sum of all cart items' discounts" do
      @calculator.total_discount.should eql(30)
    end
  end
end

Notice how the spec for the calculator does not need require spec_helper any more. Requiring spec_helper in a spec file generally means that your whole Rails application needs to load before the first example runs. Depending on your machine and application size, running a single spec may then take from a few to 30+ seconds. This is avoided with use of application preloading tools such as Spork or Spring. It is nice however if you can achieve indepedence of them.

To get more in-depth with the idea I recommend watching the Fast Rails Tests talk by Corey Haines. Code Climate has a good post on practical ways to decompose fat ActiveRecord models.

Note that new Rails 4 apps are encouraged to use concerns, which are a handy way of extracting model code that DHH recommended a while ago.

The elegance of entire suite being extremely fast, not requiring spec_helper etc is secondary in my opinion. There are many projects that can benefit from this technique even partially applied, because they have all logic in models, as in thousands of lines of code that can be extracted into separate modules or classes. Also, do not interpret the isolated example above as a call to remove all methods from models in projects of all size. Develop and use your own sense when it is a good moment to perform such architecture changes.

Using let to set context

Using let(:model) (from RSpec) in model tests may lead to unexpected results. let is lazy, so it doesn’t execute the provided block until you use the referenced variable further in your test. This can lead to errors which can make you think that there is something wrong with the database, but of course it is not; the data is not there because the let block has not been executed yet. Non-lazy let! is an alternative, but it’s not worth the trouble. Simply use before blocks to initialize data.

describe ShoppingCart do

  describe ".empty_carts" do
    let(:shopping_cart) { ShoppingCart.create }

    it "returns all empty carts" do
      # fails because let is never executed
      ShoppingCart.empty_carts.should have(1).item
    end
  end
end

On a sidenote, some people prefer not to use let at all, anywhere. The idea is that let is often used as a way to “DRY up” specs, but what is actually going on is that we’re trying to hide the complexity of the test setup, which is a code smell.

Incidental database state

Magic numbers and incidental state can sneak in specs that deal with database records.

describe Task do
  describe "#complete" do

   before do
     @task = create(:task)
   end

    it "completes the task" do
      @task.complete

      # Creating another completed task above while extending the test suite
      # would make this test fail.
      Task.completed.count.should eq(1)
    end

    # Take 2: aim to measure in relative terms:
    it "completes the task" do
      expect { @task.complete }.to change(Task.completed, :count).by(1)
    end
  end
end