This post is part of an ongoing series about Unit Testing.
Throughout this series, I’ve shown how I use test doubles in my tests.
My use of test doubles has been heavily influenced by Steve Freeman and Nat Pryce’s Growing Object-Oriented Software Guided by Tests (GOOS) and by Sandi Metz’ Practical Object-Oriented Design in Ruby (POODR).
In particular, Sandi’s focus on messages rather than objects, and on testing only the messages that cross the object’s boundaries really crystallized this for me. See her Magic Tricks of Testing talk for more.
Some people don’t like using test doubles for their tests, and that’s fine. But I think a lot of the opposition has to do with tests where doubles were used badly. When used well, I think the biggest problems can be avoided.
Let’s discuss more general advice about using test doubles well.
When using test doubles, be clear about what kind of double you’re
using. Is it a mock or a stub? In rspec-mocks, I use
expect for mocks. In my own
library for Smalltalk, I use the
stub: family of methods for stubs
expect: family for mocks.
Sometimes, I’ll stub out a method in my test setup for general use and then have one test where I set an expectation on that method to test that it really does get sent when I expect it to. Rather than testing the expectation over and over again in every test, I test it in exactly one place. The stub is there to allow the other tests to run properly.
Don’t Double Everything
Once you adopt a testing style that includes test doubles, it is tempting to use test doubles for everything. There are times, though, when a test double isn’t worth the trouble.
If I’m working with standard “primitive” and collection types in my language, I’ll just use them directly. This includes numbers, strings, booleans, arrays, hashes, etc.
Peers vs Internals
When deciding whether to use a test double, I find it valuable to think about whether the collaborating object is a peer or an internal.
Is the other object at the same level of abstraction as the object I’m testing? Does it have a life of its own outside of this usage? If so, then it’s likely a peer and I’m OK with using a test double.
Otherwise, it might be an internal implementation detail. In that case, I think hard about whether to use a test double. I want my internals to be free to change, and locking one down with a bunch of mock expectations makes that more difficult.
This advice appears in the GOOS book; J.B. Rainsberger also discusses it in a blog post.
The Object Under Test
I’ve sometimes seen a style of testing where the test provides doubles for methods of the object under test itself.
I see this most often in an event-driven or callback-driven section of code. The test will send the event in question and then check to make sure that the object responds to the event by sending itself a message. There is then a separate test that sends that message directly and tests its effects. Sometimes this chain will continue three or four levels deep into the object.
I advise against this style of testing, because it puts the internal code of the object into a straight-jacket and makes it very painful to refactor.
Instead, consider the event or callback as an incoming message just as if it was a method in the public API. Test the external effects that the event or callback will have, not the message that will be sent in response.
To repeat the advice from my previous post: ask “Why?” When you’re tempted to expect that the object will send itself a message, stop and ask yourself why you care. You probably don’t care about what message is being sent; instead, you care about the effect the method will have. Test that instead.
In general, it’s not a good idea to replace behavior on the object under test.
One case where it is tempting is if there is a low-level method that interacts with an external dependency that is hard to set up for testing, such as the filesystem or network. When I encounter this case, I look to see if it makes sense to extract that dependency to another object. If so, then I can use standard test double techniques to test my object.
The only case where I might be OK with setting a mock expectation on my object under test is where an object has a basic public API that is well tested, and then some convenience methods that combine the basic API methods in a way that needs testing. In that case, I might set expectations on the basic API methods while testing the convenience API. The basic methods are part of the object’s public API, so they are less likely to change make the expectations less fragile than if I was setting them on internal methods. It is rare that I need to do this, but I have used the technique on occasion.
Too Many Doubles
Sometimes when testing an object, you find that you need to create a large number of test doubles.
This is a sign that the object you’re testing has too many responsibilities. If it has a lot of collaborators, perhaps it is acting as a “god class” or coordinator for many other objects. Can those objects be connected to each other instead, removing some responsibilities from the object under test?
Too Many Doubled Methods
If I find I have to stub or mock a lot of methods on a collaborator, then perhaps the messages being sent to collaborator are too fine-grained. Is there a higher-level operation being performed? If so, perhaps you can create a method on the collaborator to implement that higher-level operation. Or perhaps there’s another object that could be created to encapsulate that higher-level operation.
Whenever I have too many test doubles or doubled methods, I consider it an indication that there might be something wrong with my design.
Too Much Nesting
Similar to the problem of too many test doubles is the problem of too much nesting of test doubles. When one test double stubs a method to return another test double, things start to get out of hand quickly.
One level of nesting might be OK. In Rails for example, it’s common
to stub out a class method like
find on an ActiveRecord class to
return a doubled version of the instance. I’m not thrilled with this
pattern, but Rails doesn’t offer many good alternatives.
Some test double frameworks provide a mechanism to make it easy to return a value from a stubbed chain of message sends. I try not to fall into this trap. RSpec has this feature, but the documentation makes it clear that this is really only to be used for legacy code.
If my doubles need too many layers of nesting, that’s also a sign that there is something wrong with my design. Again, is there a method I could add to a direct collaborator that hides the chain from me?
When using test doubles, you quickly learn that object creation is
tricky. When one object creates another object internally and then
uses it, it’s difficult to replace that internal object with a test
double. You might have to stub out
new on the internal class or
something like that or worse, use an
any instance double.
Once again, RSpec includes this feature with a warning.
I always look for a way to replace internal object creation with some kind of injected dependency. That’s not always good for the design, but I’ll consider it first.
Or perhaps the internal object is really an implementation detail rather than a peer. In that case, maybe I don’t have to worry about providing a test double for it.
These are some of the problems I’ve seen with using test doubles and some advice about how to avoid them.
In the next post, I’ll talk about more general testing anti-patterns.