Your typical system, written in a nice object-oriented way performs its task using a number of collaborating objects that send methods to each other. Ignoring some of the complexities like objects actually creating new objects in response to message, we can draw a diagram like the one below:
Each arrow represents a method call. Now for mocking and unit testing, usually you want to test one particular part of the system. In a lot of cases I have seen so far this would be a single object. Somewhat like this:
As we are all aware of dependencies by now and know how we inject them, the constructor for this object probably takes its two collaborators as parameters. So we can easily pass in mock objects, that are used to mimic the actual collaborators behaviour and/ or verify that the object under test actually does trigger certain actions in its collaborators:
So far so good. What is the problem with this approach?
Another thing, that the well-adjusted programmer does quite often, is refactoring. While a lot of people think of rename class/ method/ variable, when refactoring is mentioned, these are obviously the trivial cases. Let’s consider a serious change to a subsystem of our hypothetical application:
We might actually introduce some new objects to get a better factored system (contrived example). So we end up with the following:
If we actually went down the hard-core mocking road with that system the place we start from actually looks like this:
The red lines indicate interactions that are now asserted and mocked out throughout the tests. To do a refactoring like the one proposed above you have to change a whole lot of test cases. Even worse I don’t have the confidence, that the behaviour is still the same, because I had to fiddle with the tests during the refactoring, even though the “outer” interface remained the same. I call this situation test sclerosis as the tissue of your application is hardened by all the tests and mocks and thereby loses a lot of flexibility.
How to avoid this situation?
One important question that helps avoiding getting into the above mentioned situation is asking about the value of writing a particular test. Generally we write tests for the following reasons (not a complete list):
- Ensure the code actually provides the required functionality.
- Avoid regression (safety net).
- Help with design.
- Make debugging easier.
- (Make you feel confident.)
It seems to me that naive massively stubbed, or mocked unit tests usually don’t help much with these goals.
The general advice seems to be to choose your units carefully. Single objects are probably too fine grain. Look for natural subsystems. You might for example stub out the file system or an external system that has a very clearly defined interface, e.g a mail server. Keeping the number of mocks and stubs needed per test-case low (i.e. zero or one) is a good tactic. Try to test things that are interesting and non-trivial.