Gotcha While Testing Activerecord Models With Callbacks

September 18, 2012

Consider following scenario. Your domain consist of profile model that belongs to user model, and you want to assure that once user is being created appropriate profile is being created for this user, therefore there are no “profile-less” users. You can easily implement such feature using Active Record’s callbacks:

class User < ActiveRecord::Base
  has_one :profile

  before_create :build_profile

class Profile < ActiveRecord::Base
  belongs_to :user
  validates :user, :presence => true

Now you want to write profile specification that checks if profile can be successfully saved given a user. You can implement it like this:

describe Profile do
  context 'with user' do
    let(:user) { create :user }
    subject(:profile) { build :profile, user: user }

    it 'changes the count of profiles once saved' do
      expect {! }.to change { Profile.count }.by(1)

Note usage of FactoryGirl instead of fixtures.

If you run this test it will fail:


  1) Profile with user changes the count of profiles once saved
     Failure/Error: expect {! }.to change { Profile.count }.by(1)
       result should have been changed by 1, but was changed by 2

The save! method is called once, however it looks that we’ve got 2 Profile objects being saved. How could it be?

The problem is caused because let and subject in RSpec are lazily evaluated. This is what happened. First Profile.count is evaluated to fetch its current value (before it is changed). Next the expect block is evaluated. In this block we call profile which build the Profile object. While building profile user is called which creates the user. However User model has a callback creating new profile, here first profile is being saved. Next we call save! on just constructed profile saving second profile instance. Now Profile.count returns 2 instead of expected 1.

To fix the issue we need to force RSpec not to lazily evaluate creation of user object. We can do this using let! method:

let!(:user) { create :user }
blog comments powered by Disqus