October 10, 2008
Surviving Mock Abuse - My Entirely Arbitrary & Unproven Guidelines For Safe MockingThere's no doubt at all that mock objects are a powerful tool in our quest for high levels of unit test assurance, and can be very helpful in the test-driven design process.
But they can come at a price. If we overdo it, and abuse mocks, then we can end up thwarting our goal of making code easier to change.
The danger with mocking is that we are making assertions about interactions, which are often an internal design detail. This is a kind of white box testing, because it makes assertions about the internal design of methods. (Actually "grey box" would probably be a better description, because mock expectations usually reveal only some internal details). Expectations we set up for mock method calls therefore assert internal structure, rather than externally visible behaviour.
The risk is that setting up too many - or too rigid - expectations can lead to a situation where it's hard to change the internal structure of your methods. And yes, I'm talking about refactoring here.
The problem can be that abusing mocks leads you into a situation that is the exact opposite of refactoring, where your tests ensure that structure is preserved instead of the behaviour.
And I can say from experience that when developers overdo mocking, refactoring can get pretty fiddly.
When we refactor, we're often doing things like changing the targets of method calls (Move Method, Extract Class etc). And if we've got tests that expect method DoFoo() to be called on interface IFoo, and we move DoFoo() to IBar, then our test goes bang.
It's for this reason that I'm learning to follow some rough guidelines when I apply mocking, to help ensure that I don't end up making my internal designs rigid and brittle:
1. Favour small, stable interfaces for mocks. These are less likely to change as often as large interfaces that depend on a whole bunch of other evolving code. If you have to mock a large existing interface, consider refactoring it into smaller, client-specific interfaces so you can then just mock the methods you need. Consider very carefully the advice of those chaps from mockobjects.com. Interfaces imply roles, and roles imply responsibilities. What role is your mock playing in this simulated interaction with your test? What methods does it need to expose to play that role? In simpler terms - the less your tests need to know about the mocks, the less they'll need to change when your interfaces do.
2. Actually think carefully about the internal design as you write your mocking code. These interaction tests reveal all sorts about your interfaces and their dependencies. Putting more thought into their initial design will hopefully mean they'll go through fewer redesigns as the code evolves.
3. Make your mocking less prescriptive. For example, mocking tools that require interactions to happen in a specific order are arguably asking for too much information.
4. Don't think that you must mock everything except the class under test. It's absolutely fine and dandy to have real interactions going on inside your tests, just as long as your collaborators are a nice, cohesive cluster of classes.
5. Be very wary of extensive use of partial mocks of a particular class. They are trying to tell you something. Arguably, that class probably needs to be more easily substitutible, which means it might be better to declare an interface with the methods you need to mock on it.
6. Try as much as posisble to be type safe in the use of mocks. NMock sucked in this respect, since it relied too heavily on strings. RhinoMocks is much better, but if you're doing partial mocking, beware of changing constructors!
7. Remember to make assertions about behaviour! While A calls B and C calls D, amidst all those interactions your code actually does something, right? Some method somewhere is changing the internal state of your system. Contrary tpo what you may have heard, state-based testing is still absolutely necessary. That, my fine feline friend, is how we'll know that behaviour - as opposed to interactions - has been preserved when we refactor.
8. If it takes a whole stack of layered mocks (mock A has a method that returns a mock B which you then call a method on that returns a mock C etc) to get your test working, then that, too, is trying to tell you something about your design. For example, it might be telling you that you have some Middle Men in your architecture that could be refactored out, or that your architecture has too many layers. It might simply be telling you that the class your testing depends directly or indirectly on too many other classes, which is bad news for reusability, because that whole dependency stack will have to be dragged along behind it. My general advice is that if mocking feels repetitive and/or laborious, it's often because your design is out of kilter. So much do I believe this, that I'm already investigating how I could use Mock Metrics - metrics about how I'm using mocks - to help me analyse my designs.
Now I'm finding that I struggle a lot less with those inevitable refactorings, and I really feel like mocking is adding value in the TDD process.
Posted 7 years, 1 month ago on October 10, 2008