-
Notifications
You must be signed in to change notification settings - Fork 0
WhyIRarelyUseMocks
JBRainsberger recently tooted about getting people together to discuss mock objects and why certain people don't like them. Partly for my own amusement, partly in answer to his question, and partly to prepare for such a discussion should one occur, I'm writing this post.
Here is J. B.'s post:
I'm mulling hosting an open discussion about the widespread antipathy towards #TestDoubles ("#MockObjects"), because I'd like to understand more about why others have this and I don't.
It's possible that I just discovered test doubles too early in my career, wrote a lot of bad code and bad tests using them (because I wasn't a very good programmer) and bounced off of them. According to this hypothesis, my more recent code is less buggy and easier to maintain, not because it's tested without doubles, but because I'm better at programming now. Since the personal workflow I've developed doesn't involve doubles, and makes me quite productive, I'm now loath to retrace my steps and learn to use doubles again. Hence, I still think that I don't like test doubles, though I might if I put in the effort to learn to use them well.
While I think that's certainly possible, I also think there's an argument to be made for using test doubles sparingly and tactically, rather than as the default (where every collaborator of the object or function under test is mocked) or haphazardly (where there's no clear pattern to when a mock is used and when not). I'll attempt to piece together that argument in this post.
In a blog post, I explored the question: if a hand-rolled fake is a test double, then what's not a test double? The answer I landed on was "any value or object that the test controls and uses to elicit or detect some behavior of the subject". That is, every input to the system under test is a double. If I'm testing leftpad
, and pass the string "foo"
to it, then I'm using "foo"
as a test double. From the perspective of OOP, which views a string like "foo"
as an opaque object with a collection of methods, a string literal is simply a convenient way of creating a stub object with self-consistent behavior.
By that definition of "test double", I use doubles all the time, and so does every other Detroit-school tester. But that doesn't mean there's no difference between London school and Detroit school, or between the pro-mock and anti-mock camps. It just means the difference is more specific than "doubles" versus "no doubles". I do think there's a meaningful difference between the two approaches, and it's worth discussing when each might be better.
The key differences that I see between London-school and Detroit-school TDD are:
- In the London school, most test doubles use MessageBasedVerification—i.e. they're mocks or spies. In the Detroit school, which relies on StateBasedVerification, most doubles are dummies or fakes. Both schools sometimes use stubs.
- In the London school, tests focus on the behavior of a single object. Any Collaborator objects to which the test subject talks are replaced by doubles. In the Detroit school, programmers don't care, all else equal, if their tests involve one production object or several.
- Proponents of the London school like test doubles because they enable top-down implementation, where the Policy code can be implemented before the lower-level Mechanism code exists.
At the risk of putting words in people's mouths, I'll assume that the programmers who express "antipathy toward test doubles" are adherents to the Detroit school, and those who use test doubles ubiquitously are adherents to the London school. In other words, the debate between the pro-mock and anti-mock people that J. B. is talking about can be reframed as a debate between Detroit-school and London-school TDD without losing much fidelity.
First I want to talk about state-based verification versus message-based verification. I contend that most of the time, state-based verification is preferable. The advantages of state-based verification include:
- Easier and safer refactoring - you don't have to rework a bunch of tests when you change your original implementation to an equivalent one.