In the recent ‘is TDD Dead‘ debates, DHH said that he felt TDD could lead to test induced damage particularly when using mocks. He said that it can lead to layers of indirection in the codebase. I’m a long time practitioner of TDD and I frequently use mocks. I agree with DHH that these layers of indirection are bad for the codebase. I don’t think that doing TDD and using mocks always leads to this problem. Actually trying to do TDD and trying to use mocks can be quite hard to start with as you get a feel for how to use the tools appropriately. It’s quite likely that mistakes will be made as a result of try to learn TDD but I think it is important to make the distinction between damage caused by trying to learn a new technique and damage caused by using a fundamentally flawed technique.
One of the best articles I have read on TDD is the TDD Anti-Patterns catalogue. If you’re not sure if you’re doing TDD right have a read of this article and see if any of it sounds familiar. It’s a good read even if you’re fairly confident with TDD. It outlines a few patterns which describe what not to do. I’ve found this really useful when I’ve written tests or test first code that just don’t seem to be right and I’m not sure why.
Moving on, I’d like to suggest a few things you can try for using mocks with TDD which have been helpful to me.
1. Write short tests. If you can express the intent of the code you are planning to write by writing a clear and concise test first then you most likely have a good understanding of the problem.
2. If the test is starting to grow it could be that it is describing an implementation which will have too many responsibilities. Can you extract surplus responsibilities and have your test assert that they have been called on a mock rather than having the test need to know about them in detail?
3. Limit the number of dependencies for your class under test. As a rule of thumb, if I have a class with more than three dependencies I find that things start to get messy. It tends to reveal poor separation of responsibilities and the first clue is usually a long test as already mentioned.
4. Try not to mix classic TDD and mockist TDD. Testing with mocks lends itself well to testing methods that coordinate their dependencies. Testing without mocks is great for pure functions which take one or more arguments and return a value. If you find yourself writing a test which does both consider creating a method on an interface which will handle the functional part and make it a responsibility of a coordinating method to call that appropriately.
I’m not suggesting that these are hard and fast rules which you must always observe although I do suggest that you pick a small project and rigorously observe them on that to see what happens. As an aside, a few years ago I read an essay called Object Calisthenics by Jeff Bay published in the ThoughtWorks Anthology. Jeff outlines nine rules of thumb and says use them religiously for a small 1000 line project and ‘you’ll start to see a significantly different approach to designing software’. Well, I did, and I did. So in the spirit of that essay I invite you to try the above and see if you like the results.
I really believe in writing your tests first, so if you don’t already, try that too. Write a test, then write some code, then refactor. Try it. For coordinating methods this usually causes me to think about my implementation as a series of steps. I would write one or more interfaces which expose the methods I need corresponding to the steps I’ve identified. I can then test that my implementation will execute all of those steps using mocked instances of those interfaces. That series of steps must describe what I think the code should do. It must be clear and easy to understand. It is essential to get that series of steps correct. Personally I find writing a test first helps me to do this. If I make a mistake or if my test indicates too much complexity I can refactor the test and the interfaces but I won’t write any implementation code until I’m happy that my test clearly shows the direction I’m heading. If you don’t do this then you may well end up writing layers of indirection. If you get it right your interfaces will allow you to write code that defers the details of exactly how something is done to an interface which specifies a contract promising to perform a particular step. When code is written before its test or has no tests people often refactor a large method so that it calls a series of steps defined as private methods. This is pretty similar but not very testable. You need mocked interfaces for the steps in order to write a test that verifies the step was executed.
Having written plenty of code this way I’ve found that I end up with more interfaces than folk that don’t use this style. They are often fairly narrow interfaces (a term I came across in the book ‘Growing Object Oriented Software‘ which accurately describes what I see in my own code) with one or two methods. I’ve found that this gives me a loosely coupled and highly cohesive codebase. I find it easy to reuse interfaces in other parts of the codebase and often I’ll find ways to assemble the code in different and better ways as my requirements evolve.
Incidentally, as my requirements do evolve I don’t worry too much if I find there are tests which are no longer relevant. Rather than spend time refactoring irrelevant tests to somehow make them pass I’ll just throw them away. If I’ve made significant changes to an implementation I’ve usually driven them test first anyway so I know my code is covered by another test. More often I will have created a new implementation of an interface as my understanding has changed so I might leave my old tests in place as they still apply to the old implementation. If I delete the implementation I’ll have to delete the tests at the same time as they simply won’t compile any more.
To conclude then, it takes time to get good at TDD. You may very well damage your codebase as a result of trying to learn TDD so use some common sense and pay attention to the kind of tests you are writing and the kind of implementations you are writing so that you can correct your mistakes. It takes time to get good at something so stick with it until you get the results you want.