Making a mockery of ActiveRecord
December 22nd, 2006
A code walkthrough; specifying the behavior of an ActiveRecord model 'mock-first'...
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'.
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.
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.
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:
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.
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.)
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
Sorry, comments are closed for this article.

on December 22nd, 2006 at 06:08 AM
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.
on December 22nd, 2006 at 06:10 AM
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.
on December 22nd, 2006 at 01:40 PM
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.
on December 22nd, 2006 at 04:51 PM
I went ahead and updated round 3. Better.
on December 22nd, 2006 at 07:49 PM
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:
on December 22nd, 2006 at 07:50 PM
OK that didn’t work, here is a pastie – I put this in spec_helper.rb:
http://pastie.caboo.se/29282
on December 22nd, 2006 at 09:59 PM
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.
on December 23rd, 2006 at 06:08 AM
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 :)
on December 23rd, 2006 at 09:04 PM
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
on December 24th, 2006 at 11:44 AM
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 endon December 26th, 2006 at 09:32 AM
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 endThis 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.