Totally Bogus Doubles for Tests
I love me some mocks and stubs. Like many people, I didn't really get
the idea until I did some reading about the technique. And then I didn't really get it until I did some more reading. And even then I didn't really get it until I re-wrote a suite of tests like 3 times, each time getting closer to that coding nirvana: a blazingly fast group of tests that exposes dependencies and is resilient to change.
A big step for me was implementing real testing doubles, moving away
from tiny structs and actually creating a Ruby class to act as my
double. The functionality and reusability of these doubles is great. But
they introduce a problem. For example, take this dumb code:
class Record
attr_accessor :number
def initialize
#A ton of db work
end
def length
# even more db work
end
end
class RecordDouble
attr_accessor :number
def length
@length ||= rand(50)
end
end
Now I can use RecordDouble as a collaborator in a test and not have to
incurr the pain of creating a real record instance. Yay.
class RecordCollaboratorTest
before :each do
@record_double = RecordDouble.new
@it = RecordCollaborator.new(@record_double)
end
it "should use record length in some way" do
assert_equal @record_double.length, @it.length
end
end
And all is good. Then I change the length method on the Record object:
class Record
...
def length(options_hash)
end
end
And a varitey of bad things happen, right? My tests no longer document
how to use my code. My tests pass, but production could fail. There's
drift, and drift = rot = badness.
And even if that never happens, using doubles like this is duplication.
I have two classes that have (or should have) the same interface. Every
change in one needs to be made in the other.
A quick solution I implemented when I did this the first time came, more
or less, from Sandi Metz's book.
class RecordTest
setup do
@it = RecordDouble.new
end
include RecordInterfaceTest
...
end
class RecordDoubleTest
setup do
@it = RecordDouble.new
end
include RecordInterfaceTest
...
end
module RecordInterfaceTest
[:number, :id, length].each |m|
assert @it.responds_to?(m)
end
end
Still duplication (actually now triplication of the API methods), but at
least I could control drift. Kind of. If Record or RecordDouble stopped
supporting a method that was in the interface test, an alarm bell would
go off. But if arity changed, I wouldn't notice.
Fast forward several months and I find myself implementing this pattern
again in a different code base. But now I have this video, in which (at the 29 minute mark), Sandi Metz points to four (quacky, bogus, rspec-fire, minitest-firemock) gems that can solve this problem in a better way.
After reviewing the options, I chose Bogus. Its use seemed the cleanest
and it appeared to have solid documentation. I eventually found that documentation to be somewhat lacking, hence this blog post.
First, I'm implementing this with MiniTest spec, and their documentation
leaned more to the Rspec crowd. And I'm testing ActiveRecord objects,
while their documentation focused on plain Ruby code. Those little
wrinkles can trip you up. They certainly tripped me up.
Getting your 'test_helper' in shape
I created a new helper file
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'minitest/autorun'
require 'bogus/minitest/spec'
On to the test
describe Fruit do
fake(:garden)
fake(:trellis) { Support::Trellis }
describe "#initialize" do
before do
stub(garden).fertilized? { true }
stub(trellis).material { 'wood' }
end
...
end
end
Here I'll be testing a Fruit object. It has two collaborators I need to
double: Garden and Trellis. Here you see the two ways you can call the
Bogus fake method. In the first I pass no block, so Bogus assumes I want
to fake an instance of Garden. But for trellis, I pass in the class I
want.
All of the examples in Bogus use the first method. The 2nd is
documented, but I was so busy looking at the examples that I didn't notice.
Now the magic
The result of fakes is that I can use garden
and trellis
in
my tests and stub methods on them. And that separate directory of double
classes? Gone. The library of interface tests? Gone. All I have now are
the real object defitions that I fake during tests.
And the magic has a posse
But if I try to stub something that the classes don't support, Bogus will throw an error.
stub(garden).disco_party? { true} => #NameError: #<Garden:0x3fea091c41b0> does not respond to disco_party?
And arity is respected:
stub(trellis).leg_number(1, 'huh?') { nil } => ArgumentError: tried to stub leg_number(count) with arguments: 1,'huh?'
But maybe not magic enough?
This is all excellent and much appreciated. Now I can create doubles
with ease and not worry about API drift. Let's throw them on some
ActiveRecord models. Here's a Fee that has a value code that's persisted
in a database:
irb> f = Fee.first
=> #<Fee code: 100>
Let's double that and use it in a test:
describe FeeCollaborator do
fake(:fee)
describe "#initialize" do
before do
stub(fee).code { 999 }
end
...
end
end
And instead of a happy shiny double, you'll get:
NameError: #<Fee:0x3fc3b60eed2c> does not respond to code
But Fee obviously responds to code, I just saw it in the console! And
here's where the Bogus documentation is, pardon me for saying it,
slightly bogus. Under Configuration Options, theres a link for
Fakearattributes which will tell you why this is breaking and how to fix it. In short, just put this in your initial test_helper file:
Bogus.configure do |c|
c.fake_ar_attributes = true
end
And your tests of ActiveRecord objects will work.
There's a lot more Bogus features that I'm looking forward to trying.
Spies, contracts and argument matchers all look great. And if I find any
tricks to implementing those features, I'll post them here.