Hi David,
On 29 May 2009, at 12:10, David C. wrote:
Have you read “Mock Roles, not Objects”?
“its corresponding mock” suggests a single mock object for each real
implementation. There’s nothing stopping you from writing your own
mock objects to play this role. IMO, this is not what a dynamic mock
objects framework is for.
Agreed – I entirely concur that the main benefit of such a framework
is the ability to speculatively mock collaborators (while thinking in
terms of their role) before you have implemented them or perhaps even
know what they are – but in practice I find that these collaborating
roles end up falling into one-one correspondence with actual classes,
which doesn’t seem to contradict the core message of “Mock Roles, not
Objects”. Maybe that just indicates I’m not working on designs that
are sufficiently sophisticated, sufficiently polymorphic or
sufficiently aspect-oriented to feel the burn of the divergence of
concepts.
Ultimately I think I’m agreeing with you, but perhaps just using the
wrong word: I might better have described Account as a role rather
than necessarily an object/class. The original point applies, namely
“how can I be sure that my mock (for a given role) is in sync with my
specification (of that role in the object(s) which perform it)”;
instead of suggesting a single mock object for each real
implementation, I’m suggesting a single mock object for each role.
And speaking of using the wrong words: maybe what I actually mean is
“specifying stubs” instead of “specifying mocks”, in as much as those
global helpers I talked about don’t really return a “mock” at all,
just a stub which has been prepared for use as a mock in any spec
which requires it (cf Spec::Rails’ mock_model). But any such spec will
exercise those stubbed methods which are important for the role’s
behaviour, assuming one expectation per example, so it’s important
that the stubbed behaviour accurately reflects reality, and unless you
are rigorous about keeping the stubbed behaviour updated to reflect
the specified behaviour they’ll drift out of sync and the specs won’t
be making the right assumptions about collaborators any more.
Just because two objects sport the same methods doesn’t
mean they behave the same way. In order to verify that a mock Account
honors the same contract as a real Account, you’d have to have some
very general specs like “Account balance should be an instance of the
Money class.”
Why? I can’t quite get my head around the issues of stubbing stateful
interactions, which is preventing me from thinking of a good example,
but what’s (abstractly) wrong with:
class Account
def credit(cents)
self.balance = self.balance + cents
return self.balance
end
end
describe ‘Account#credit’ do
before(:each) do
@account = Account.new(:balance => 100)
end
specify { @account.credit(10).should == 110 }
end
def mock_account
account = mock(‘Account’)
account.stub!(:credit).with(10).and_return(110)
return account
end
It seems like there’s nothing stopping you stubbing the detailed
behaviour of Account in a way that will allow the stub to pass the
specs – it’s just that the stub is only good for the canned
responses, necessarily a strict subset of the real implementation’s
behaviour over all inputs.
Given the above, I could easily change spec and implementation to
class Account
def credit(cents)
self.balance = self.balance - cents
return self.balance
end
end
describe ‘Account#credit’ do
before(:each) do
@account = Account.new(:balance => 100)
end
specify { @account.credit(10).should == 90 }
end
and the spec will pass, and so will every spec that’s using my Account
stub for mocking Account behaviour, but the system as a whole is
screwed, right? Because the behaviour’s changed but the stub (plus the
behaviour that is mocked on it by other specs) has stayed the same.
So really, what we’re talking about is concern over method
signatures straying.
Sorry, that’s not what I meant at all: I intended to talk specifically
about cases where the signature stays the same but the underlying
behaviour changes.
What about when we use mocks in their most powerful way, to specify
an object’s
contract with polymorphic collaborators?
In that case there’s a role (an object fulfilling that contract) for
which you might end up creating a shared stub that’s used in the
specifications of all objects which act as the client in that specific
collaboration. Individual examples in those specifications will set
individual expectations on that stub in order to specify the
interaction, presumably clobbering a stubbed method in the process,
but the remaining stubbed methods will be exercised during each
example, which gives you a way of knowing that the mocked behaviour
lines up with what’s been canned in the stub.
You can’t do much about the detailed, piecemeal mocking that happens
inside such specs – aside from the aforementioned reliance upon the
parts of the stub’s canned behaviour for which an expectation hasn’t
explicitly been set, there’s no way to check that the expectations
match up with the specification(s) of the object(s) which performs the
role you’re mocking, so you have to rely on integration specs – but
if all of these specs are starting with the same stub, you can at
least run the role’s spec over the stub to make sure it’s accurate.
Why wouldn’t you?
Personal opinion aside, let’s say we set out to solve this. We’d need
some auditing mechanism that says the object being mocked has all the
same APIs as the mock object.
As I say, that’s not what I meant, and it’s probably my fault for
saying “mock”. What I meant is that a stub can provide a snapshot of
all of the behaviour of an object (or role), and I often end up in the
situation where the same stub is being used as a starting point for
mocking interactions in lots of specs, but right now there’s no
mechanism for spotting when that stub is spreading misinformation
because it’s become desynchronised from the specification of the
object (or role) which it represents.
For example: Spec::Rails’ mock_model. (Should be called stub_model but
isn’t.) It’s very convenient because it stubs out a bunch of
“behaviour” that you’d otherwise have to stub out yourself in all of
your model specs before you started adding expectations. It conforms
to a tiny subset of ActiveRecord::Base’s notional specification:
@record.should_receive(:new_record?).and_return(false) etc. If “the
ActiveRecord::Base spec” changes, mock_model needs updating, but we
can’t discover that automatically because we never run “the
ActiveRecord::Base spec” over the object that’s returned by
mock_model. (This is a stupid example because there is no
ActiveRecord::Base spec, but you get the idea.)
So is my fundamental mistake that I’m being lazy by trying to
concentrate all of this stub setup in one helper method instead of
doing piecemeal stubbing in every individual spec that might care? It
seems superficially like good practice to concentrate all of the
detail in one place like this, so that when a role’s behaviour changes
you just update one piece of stub setup to reflect the change rather
than chase around the specs of every single collaborator, but maybe
that’s just serving to obscure the problem that all of the actual
mocking has to happen in the individual specs and that this is the
stuff you really care about, not the behaviour of the shared stub?
Cheers,
-Tom