On Thu, Sep 4, 2008 at 4:14 PM, Nick H. [email protected]
wrote:
minute, would you mind countering me?
Hey Nick,
I’ve talked with many people that echo your concern that mocks couple
specs to an object’s implementation. It’s a legitimate concern, of
course, however, what people tend to fail to recognize is that at some
level, specs are always coupled to implementation. Let’s consider
for a moment what a specification is: a statement describing expected
behavior. In programming terms, what an object does. Well, what
does an object do, anyway?
There are three basic things an object can do:
- It can respond to a message
- It can return a result as the response to a message
- It can interact with collaborators
Let’s look at a super basic spec example and its associated object
(just typing it up, please excuse typos):
describe BankService, “#debit” do
before(:each) do
@account = Account.new(100)
@service = BankService.new
end
it “should debit the account” do
@service.debit @account, 25
@account.balance.should == 75
end
end
class BankService
def debit(account, amount)
account.debit amount
end
end
Now this example is totally contrived - there’s nothing going on,
there’s just a middleman delegating a call to another object. But
let’s take a look at it anyway. What are the changes that could cause
this spec to fail? I can think of several:
- BankService.new changes signature
- BankService#debit gets renamed or changes signature
- Account#debit gets renamed or changes signature
- Account.new changes signature
- Account.debit changes implementation (e.g. #debit also applies some
kind of charge, resulting in #balance returning a different result)
That, to me, represents a serious problem. Out of five ways in which
this spec could break, only TWO of them are related to the Unit Under
Test (and this doesn’t include a name/signature change to
Account#balance)
But what happens if we use a mock object instead?
describe BankService, “#debit” do
before(:each) do
@mock_account = mock(“account”)
@service = BankService.new
end
it “should debit the account” do
@mock_account.should_receive(:debit).with(25)
@service.debit @account, 25
end
end
- BankService.new changes signature (spec fails)
- BankService#debit gets renamed or changes signature (spec fails)
- Account#debit gets renamed or changes signature (spec still passes)
- Account.new changes signature (spec still passes)
- Account.debit changes implementation (spec still passes)
By using a mock object, we’ve reduced the number of potential failure
causes from 5 to 2. Now, I will grant you that #3 (Account#debit gets
renamed or changes signature) may result in a false positive, which is
a Bad Thing. It’s a false positive in the sense that the system as a
whole doesn’t work though, not that there’s something wrong with the
BankService object itself. This is why we need integration tests.
But basically, as long as the BankService’s logic stays correct, the
existing specs pass. And this is what happens when there’s basically
no logic - it’s all delegation - so imagine what happens when we have
real logic and multiple collaborators!
So, any time you write a spec for an object that has one or more
collaborators, you must ask yourself the following question: “Do I
want my specs to be coupled to this object, or do I want my specs to
be coupled to this object’s collaborators?” The problem with choosing
the second option is that whenever you’re coupled to an object’s
collaborators, you’re also coupled to the collaborators’
collaborators!
When you use mock objects, your specs ensure that you’re coupled only
to collaborators’ interfaces and not to their interfaces AND
implementations. That’s nice, because collaborators (= dependencies)
have their OWN collaborators (=dependencies), so using real
collaborating objects in specs means that you’ve introduced a
dependency, and all its dependencies, and all its dependencies’
dependencies, ad infinitum. So which spec is more brittle in reality?
The one that breaks whenever the UUT changes, or the one that breaks
whenever one of X nested dependent objects changed?
So there we go, a lengthy (but hopefully useful) explanation of why
mock objects are good for object-level specs aka unit tests. In
short, an interaction-based spec is coupled to its collaborators’
interfaces, and a purely state-based spec is coupled to its
collaborators’ interfaces and implementations, recursively until
there are no more collaborators.
That’s that. And there’s another piece to this whole “mocks couple
your specs to implementation” thing, which is that whenever I notice
developers being slowed down by mocks, it’s usually because they’re
missing an abstraction. There’s a great paper by Steve Freeman and
Nat Pryce called “mock roles not objects” [1] that gets into this.
Basically, you’ll only have success mocking useful abstractions, not
low-level stuff. That is to say, mocking File.read might eliminate
your test’s dependency on the filesystem, but it doesn’t change the
fact that your object is dealing with a relatively low-level operation
- and ultimately, we care about the design and effectiveness about the
production code itself rather than the tests. So instead of mocking
out File.read, you spec out a ConfigReader class (for example) that
actually reads and parses some file, and once that’s complete you use
mock objects in any spec that needs a ConfigReader instance.
Unfortunately it’s late and I’ve run completely out of steam and can’t
write anymore / create examples.
Bullet points
- Your tests are always coupled to an implementation at some level
- Mocks reduce the number of potential failure causes by eliminating
dependencies
- Pain when mocking usually points to potential design improvements
I encourage you to voice any other comments or concerns you’ve got,
and to point out the holes in my thinking.
Pat
[1] (PDF) http://www.jmock.org/oopsla2004.pdf