How to test callback methods in Rails

ActiveRecord models come with useful callbacks like after_save and before_validation. Here is how to properly test your callback methods.

Our example will be a class Project. Each project has an owner and many milestones. After saving, the project creates an initial milestone and notifies the owner of its creation.

class Project

  belongs_to :owner
  has_many :milestones

  after_save :create_milestones
  after_save :notify_owner

  private

  def notify_owner
    owner.project_created!
  end

  def create_milestones
    milestones.create(:name => 'Milestone 1')
  end

end

Here is a bad example of how to test this class:

describe Model, 'after_save' do

  it 'should create an initial milestone' do
    project = Project.new
    project.milestones.should_receive(:create)
    project.run_callbacks(:after_save)
  end

  it 'should notify its owner' do
    project = Project.new(:owner => mock_model(User))
    project.owner.should_receive(:project_created!)
    project.run_callbacks(:after_save)
  end

end

The test is bad because it tests too much at once. Each test has to deal with the side effects of every other after_save method. The first example will actually crash because it calls project_created on an owner that is nil.

You would much rather test the behaviour of each callback method in isolation. Then add another test that check whether all expected methods are called.

describe Project do

  describe 'create_milestones' do
    it 'should create an initial milestone' do
      project = Project.new
      project.milestones.should_receive(:create)
      project.send(:create_milestones)
    end
  end

  describe 'notify_owner' do
    it 'should notify its owner' do
      project = Project.new(:owner => mock_model(User))
      project.owner.should_receive(:project_created!)      
      project.send(:notify_owner)
    end
  end

  describe 'after_save' do
    it 'should run the proper callbacks' do
      project = Project.new
      project.should_receive(:create_milestones)
      project.should_receive(:notify_owner)
      project.run_callbacks(:after_save)
    end
  end

end

Always try to only test one thing at a time. It will keep your examples focused and more resilient to change.

You can follow any response to this post through the Atom feed.

Avatar

Sat, 06 Mar 2010 11:26:00 GMT

by henning

Tags:

  • Sheldon Hearn said almost 2 years later:

    This presents a conundrum that I struggle with ongoingly. Should I be testing that when I call method X, method Y is called? Or should I be testing that when I call method X, state Z is achieved.

    First prize, as is often the case, is not either or, but both. Test that when I call method X, method Y is called, and then test that when I call method Y, state Z is achieved. Now I get all the benefit of both approaches. I know that:

    • method Y is a side effect of method X,
    • state Z is the intended goal of method Y, and
    • by inference, state Z is a side effect of method X.

    All explicit, all proven.

    That is exactly what you’ve done here, which I like a lot. However, your explanation of your choice of test isolation wasn’t explicit enough for me, so I added this comment.

    Test isolation only exploded in my mind like a fireworks display when @rorymckinley started giving me concrete explanations like this in our work together.

Leave a comment