Stub Queries and Expect Commands

Posted by James Mead Sat, 23 Dec 2006 20:02:00 GMT

In Making a Mockery of ActiveRecord, a single test has multiple expectations set up for accessor methods…

  specify "should create EmailMessages and Subscriptions when include_subscribers is true" do
    @message.include_subscribers = true
    @message.should_receive(:lists).and_return([@list])
    @message.should_receive(:campaign).twice.and_return(@campaign)
    @list.should_receive(:people).and_return([@person])
    @person.should_receive(:campaigns).and_return([])

    @person.should_receive(:subscriptions).and_return(@subscriptions)
    @subscriptions.should_receive(:create).and_return(nil)
    @campaign.should_receive(:people).and_return([])

    message.generate # I think this line is missing from the original test
    @message.should_have(1).email_messages
  end

I’ve found Nat Pryce’s rule of thumb – stub queries and expect commands – to be very useful in reducing the brittleness of tests (see Yoga for Your Unit Tests).

A query in this context is a method which does not change the state of the object on which it is called. The accessor methods definitely fall into this category, so I would stub them, not set expectations for them. The one command method in the above example which merits an expectation is the call to create on @subscriptions.

However, I prefer to use in-memory ActiveRecord objects instead of mocks or stubs wherever possible. So I’d write something more like this (using Test::Unit and Mocha)...

  def test_should_create_email_messages_and_subscriptions_when_include_subscribers_is_true
    subscriptions = mock()
    person = Person.new(:campaigns => [])
    person.stubs(:subscriptions).returns(subscriptions)
    list = List.new(:people => [person])
    campaign = Campaign.new(:people => [])
    message = Message.new(:lists => [list], :campaign => campaign)
    message.include_subscribers = true

    subscriptions.expects(:create)

    message.generate
    assert_equal 1, message.email_messages
  end

I haven’t actually run this – so it’s quite likely there are errors in it, but you should get the general idea. The person.stubs line is necessary to avoid the type checking that Luke Redpath mentions in his comment.

More thoughts here.

Tags , , , , , ,  | 1 comment

Fed up with Rails fixtures? (part one)

Posted by James Mead Thu, 10 Aug 2006 04:09:00 GMT

In a previous post I explained why I’m not a big fan of Rails fixtures. But how can you avoid using them and still obtain good test coverage? Try forgetting about fixtures and writing unit tests that only test a single class and nothing else. Here’s an example where you hardly1 need to involve the database at all…

class Initial < ActiveRecord::Migration 
  def self.up 
    create_table :companies do |t| 
      t.column :name, :string 
    end
    create_table :employees do |t|
      t.column :company_id, :integer
      t.column :salary, :integer
    end
  end
end

class Company < ActiveRecord::Base
  has_many :employees
  def wage_bill
    employees.inject(0) { |total, employee| total + employee.salary }
  end
end

class Employee < ActiveRecord::Base
  belongs_to :company
end

So instead of writing this…

# companies.yml
walmart:
  id: 1
  name: Walmart

# employees.yml
fred:
  id: 1
  company_id: 1
  salary: 10000
anne:
  id: 2
  company_id: 1
  salary: 20000
class CompanyTest < Test::Unit::TestCase
  fixtures :companies, :employees
  def test_should_calculate_wage_bill
    assert_equal 30000, companies(:walmart).wage_bill
  end
end

You can write this…

class CompanyTest < Test::Unit::TestCase
  def test_should_calculate_wage_bill
    employees = []
    employees << Employee.new(:salary => 10000)
    employees << Employee.new(:salary => 20000)
    company = Company.new(:employees => employees)
    assert_equal 10000 + 20000, company.wage_bill
  end
end

This way you home in on the behaviour of the Company#wage_bill method and avoid testing the ActiveRecord code and access to the database. More ways to avoid hitting the database will follow in another post.

1 ActiveRecord still needs to know what columns in each table e.g.

SHOW FIELDS FROM companies

Tags , , , , , ,  | 3 comments