Making a mockery of ActiveRecord

December 22nd, 2006

A code walkthrough; specifying the behavior of an ActiveRecord model 'mock-first'...

This is long, so grab a drink

I had a good RSpec experience today (well, it is a weekday) that I thought was worth sharing.

If this is your first time reading about mocks, or if you're just rusty, you may want to hit up this nice collection of info: Ruby Mock Objects

The setup:
  • An email Campaign.
  • A Message that represents what is arguably a template.
  • People and semi-dynamic Lists of people.
  • People can be connected to a Campaign directly via a Subscription (an opt-in), or they can simply be on a List.
    (We won't see the reasoning for that in this simplified example, but just pretend it makes sense.)
When Messages are ready to be delivered, they are told to generate EmailMessage records.
EmailMessages are connected directly to People, and represent what will eventually be sent as real emails.

If this seems involved, it's because you haven't seen the customer requirements, so stop complaining.

The traditional Rails solution to testing this would be to make fixtures for all of the above models, and any join models.
You would then load and roll back those fixtures numerous times, repetitively testing what you already know; namely, that your database service has been started properly.

This has a number of drawbacks, not least of which is that you are probably referring back to your fixtures as you write your tests, to make sure you get the names right, and that you pick the appropriate fixture for each scenario.

Enter Mocks.

Even in an alternate world where mocks were no more expressive than fixtures, they would be worth using simply to have your 'setup' information right there on screen as you write your specs. Luckily, they are a lot cooler than that.
I'll take you through a tour of the various steps along the way to specifying the behavior of the EmailMessage generation process, 'mock-first'.

Round One

Here is the first cut at the code.

 1 context "A Message being generated for delivery" do
 2   setup do
 3     @campaign = mock("campaign")
 4     @campaign.stub!(:is_a?).and_return(true)
 5     @campaign.stub!(:new_record?).and_return(false)
 6     @campaign.stub!(:id).and_return(rand(1000))
 7 
 8     @person = mock("person")
 9     @person.stub!(:is_a?).and_return(true)
10     @person.stub!(:new_record?).and_return(false)
11     @person.stub!(:id).and_return(rand(1000))
12 
13     @message = Message.new :name => "Hello, world", :campaign => @campaign
14     @list = mock("list")
15     @subscriptions = mock("subscriptions")
16   end
17 
18   specify "should create EmailMessages but not Subscriptions when include_subscribers is false" do
19     @message.should_receive(:lists).and_return([@list])
20     @list.should_receive(:people).and_return([@person])
21     @message.should_receive(:campaign).and_return(@campaign)
22     @person.should_receive(:campaigns).and_return([@campaign])
23     @message.generate.should == 1
24     @message.should_have(1).email_messages
25   end
26 
27   specify "should create EmailMessages and Subscriptions when include_subscribers is true" do
28     @message.include_subscribers = true
29     @message.should_receive(:lists).and_return([@list])
30     @message.should_receive(:campaign).twice.and_return(@campaign)
31     @list.should_receive(:people).and_return([@person])
32     @person.should_receive(:campaigns).and_return([])
33 
34     @person.should_receive(:subscriptions).and_return(@subscriptions)
35     @subscriptions.should_receive(:create).and_return(nil)
36     @campaign.should_receive(:people).and_return([])
37     @message.should_have(1).email_messages
38   end
39 end
40 

This is pretty decent, really. Probably fewer lines of code than the equivalent fixtures, and faster because we're not hitting the database.
On the other hand, there's 'setup' everywhere, and it's hard to see what the actual expectations are.
@message.should_have(1).email_messages is like a needle in a haystack.

Also, I've got everything in a single 'context', even though quite literally there are two different ones in play here.
I simply didn't want to have to repeat the 'setup' block in a second context, and got lazy.

Let's fix that first, since maybe it's at the heart of the problem.

Round Two


 1 module MessageSetup
 2   def setup_message
 3     @campaign = mock("campaign")
 4     @campaign.stub!(:is_a?).and_return(true)
 5     @campaign.stub!(:new_record?).and_return(false)
 6     @campaign.stub!(:id).and_return(rand(1000))
 7 
 8     @person = mock("person")
 9     @person.stub!(:is_a?).and_return(true)
10     @person.stub!(:new_record?).and_return(false)
11     @person.stub!(:id).and_return(rand(1000))
12 
13     @message = Message.new :name => "Hello, world", :campaign => @campaign
14     @list = mock("list")
15     @subscriptions = mock("subscriptions")
16   end
17 end
18 
19 context "A Message being generated without include_subscribers" do
20   include MessageSetup
21   setup { setup_message }
22 
23   specify "should create EmailMessages but not Subscriptions" do
24     @message.should_receive(:lists).and_return([@list])
25     @list.should_receive(:people).and_return([@person])
26     @message.should_receive(:campaign).and_return(@campaign)
27     @person.should_receive(:campaigns).and_return([@campaign])
28     @message.generate.should == 1
29     @message.should_have(1).email_messages
30   end
31 end
32 
33 context "A Message being generated with include_subscribers enabled" do
34   include MessageSetup
35   setup { setup_message }
36 
37   specify "should create Subscriptions and EmailMessages" do
38     @message.include_subscribers = true
39     @message.should_receive(:lists).and_return([@list])
40     @message.should_receive(:campaign).twice.and_return(@campaign)
41     @list.should_receive(:people).and_return([@person])
42     @person.should_receive(:campaigns).and_return([])
43 
44     @person.should_receive(:subscriptions).and_return(@subscriptions)
45     @subscriptions.should_receive(:create).and_return(nil)
46     @campaign.should_receive(:people).and_return([])
47     @message.generate.should == 1
48     @message.should_have(1).email_messages
49   end
50 end
51 

Here I've taken the 'setup' code, moved it into a module, and mixed it into the two contexts.
It looks a little showy, but at least it keep the setup code away from the specs.
Also, now that there are two separate contexts, the specs can be a little more readable.

All that being said, I'm still not happy. The ugly stubs required to fake out ActiveRecord are distracting, and appear in every context that needs to say:
@real_activerecord_model.some_association = some_mock

Under the hood, ActiveRecord checks to make sure the association is being handed a compatible object. We have to stub that to make this all work. Why are we bothering again? Test isolation. Future model changes that don't affect this code shouldn't break these specs. If you are dealing with six or seven model classes per spec, that is ridiculously easy to do.

What can we do about this boilerplate? Well, let's make a helper.

Round Three


 1 module MessageSetup
 2   def setup_message
 3     mock_model :campaign
 4     mock_model :person do |m|
 5       m.should_receive(:campaigns).and_return([@campaign])
 6     end
 7     mock_model :list do |m|
 8       m.should_receive(:people).and_return([@person])
 9     end
10     @message = Message.new :name => "Hello, world", :campaign => @campaign
11   end
12 end
13 
14 context "A Message being generated without include_subscribers" do
15   include MessageSetup
16   setup { setup_message }
17 
18   specify "should create EmailMessages but not Subscriptions" do
19     @message.should_receive(:lists).and_return([@list])
20     @message.should_receive(:campaign).and_return(@campaign)
21     @message.generate.should == 1
22     @message.should_have(1).email_messages
23   end
24 end
25 
26 context "A Message being generated with include_subscribers enabled" do
27   include MessageSetup
28   setup do
29     setup_message
30     @subscriptions = mock("subscriptions")
31     @campaign.should_receive(:people).and_return([])
32     @person.should_receive(:subscriptions).and_return(@subscriptions)
33     @message.include_subscribers = true
34   end
35 
36   specify "should create Subscriptions and EmailMessages" do
37     @message.should_receive(:lists).and_return([@list])
38     @message.should_receive(:campaign).twice.and_return(@campaign)
39 
40     @subscriptions.should_receive(:create).and_return(nil)
41     @message.generate.should == 1
42     @message.should_have(1).email_messages
43   end
44 end
45 

Hey, that's looking pretty good. "mock_model" says pretty directly what the mission is, and having it take a block avoids the need to repeat the name of the instance variable.
In fact, there's no longer a need to assign the mock return value to something, since, in typical 'convention over configuration' style, the mock automatically goes into an instance variable with the same name as the model.

How did we do? Let's see what the "time" command has to say:
time drbspec spec/models/message_spec.rb

.............

Finished in 0.268692 seconds

13 specifications, 0 failures
drbspec spec/models/message_spec.rb  
0.08s user 
0.02s system 
11% cpu 
0.852 total

Under a second of wall-clock time isn't half bad.

Here's the helper code, which goes in the 'EvalContext' of spec_helper.rb.
I'll probably be adding some more features before submitting it as a patch, but you're welcome to it.

 1 def mock_model(name)
 2   name = name.to_s
 3   m = mock(name)
 4   instance_variable_set("@#{name}", m)
 5   m.stub!(:id).and_return(rand(10_000))
 6   m.stub!(:new_record?).and_return(false)
 7   klass = name.singularize.camelize
 8   m.send(:__mock_handler).instance_eval <<-CODE
 9           def @target.is_a?(other)
10             other == #{klass}
11           end
12           def @target.class
13             #{klass}
14           end
15   CODE
16   yield m if block_given?
17 end

11 Responses Follows

  1. John Turner says

    I prefer Round Two here at the moment, with the separation between the setup and the tests. Round Three has gotten pretty cluttered and you’re repeating a lot of code. Should probably go back to using the setup mixin really.

  2. John Turner says

    Sorry, to clarify that, I’m not saying just go back to Round Two, but move to something that uses the helper method, but does so in a mixin.

  3. Wilson Bilkovich says

    Yeah, you’re probably right. I do like the way the mixin version reads. I just didn’t think anyone would read all the way to the bottom if I had a ‘Round Four’. Thanks for checking it out.

  4. Wilson Bilkovich says

    I went ahead and updated round 3. Better.

  5. Luke Redpath says

    That you had to work around the explicit type check of ActiveRecord associations has always bugged me – why check for type in a language that not only uses but actively promotes duck typing?

    My solution to this has been to write a small monkey patch to ActiveRecord that gets rid of this idiocy:

    1. lets kill that blasted activerecord type checking! module ActiveRecord module Associations class AssociationProxy def raise_on_type_mismatch(record) # bite me! end end end end
  6. Luke Redpath says

    OK that didn’t work, here is a pastie – I put this in spec_helper.rb:

    http://pastie.caboo.se/29282

  7. Wilson Bilkovich says

    Haha. Nice one, Luke. Conceptually, I guess I’d like to avoid modifying AR, to avoid hiding bugs.. but that does seem like a nice trick.

  8. Pat Maddox says

    Great post Wilson. I especially like mock_model for getting rid of the boilerplate. The blocks in setup_message should probably call stub! instead of should_receive though. Looking forward to more posts :)

  9. James says

    I hope you don’t mind, but I’ve written up a few thoughts…

    http://blog.floehopper.org/articles/2006/12/23/fixtures-mock-objects-or-in-memory-activerecord-objects http://blog.floehopper.org/articles/2006/12/23/stub-queries-and-expect-commands http://blog.floehopper.org/articles/2006/12/23/tell-dont-ask

  10. Wilson Bilkovich says

    James: Interesting. Thanks for thinking so much about my article. I wrote it partly to help me think about whether I was going about things the right way. Your way might very well be cleaner.

    I was going to put this reply on your blog, but I’m having routing trouble getting it to load suddenly.

    There are definitely some rough edges in AR where things are passed by ‘dup’ing, rather than by reference. Pays to keep that in mind.

    Just in case it helps, here’s the implementation that is being spec’d out:

    You’re right that the Association Proxy setup in ActiveRecord makes it hard to obey the law of demeter.
    def recipients
      recipients = self.include_subscribers? ? self.campaign.people.to_a : []
      recipients << self.lists.collect(&:people).flatten
      recipients.flatten.uniq
    end
    
    def generate_email_messages
      count = 0
      people = self.recipients
      Message.transaction do
        people.each do |person|
          unless person.campaigns.include?(self.campaign)
            person.subscriptions.create :campaign => self
          end
          em = self.email_messages.create :person => person, :message => self, 
            :direction => 'mt', :sent_at => Time.now.utc, :body => self.body
          count += 1 if em.valid?
        end
        if count != people.size
          raise RuntimeError.new("There were #{people.size} intended recipients, but only #{count} Email Messages were valid. Aborted.")
        end
      end
      count
    end
    
  11. David Chelimsky says

    Nice post and great to see all the comments here. Like you, I like to keep method stubs in setup and mock expectations in the specs, but I approach it slightly differently. The mock framework in rspec lets you temporarily override a stub with a mock method. So I use method stubs in the setup to take care of everything necessary to make the spec run. Then, in each spec, I use mock expectations to specify a small piece of what is expected.

    This would allow you to do this:

    context "A Message being generated without include_subscribers" do
      include MessageSetup
      setup { setup_message }
    
      specify "should create EmailMessages" do
        ...
      end
    
      specify "should not create Subscriptions" do
        ...
      end
    end
    

    This allows you to isolate what you’re spec’ing in a very granular way without gumming up each spec w/ a bunch of setup that is necessary to make things run, but not necessary to help you understand what you are spec’ing.

Sorry, comments are closed for this article.