I have blogged about TDD before. I think that it is one of the most important tools in improving the design of our software, as well as increasing the quality of the systems that we create. TDD provides valuable, fine-grained feedback as we evolve the solutions to the problems that our code is meant to address.
Oh yes, and as a side-benefit, you get some nice efficient, loosely coupled, tests that you can use to find regression problems in future. 😉
I sometimes teach people how to practice TDD more effectively, and one of the things that I notice is that one subtlety that people often miss is the difference in focus for each of the TDD steps.
True TDD is very simple, it is “RED, GREEN, REFACTOR“.
- We write a test, run it and see it fail (RED).
- We write the minimum code to make it pass, run it and see it pass (GREEN).
- We refactor the code, and the test, to make them as clean, expressive, elegant and simple as we can imagine (REFACTOR).
These steps are important not just as a teaching aid, but also because they represent three distinct phases in the design of our code. We should be thinking differently during each of these steps…
We should be wholly focussed on expressing the behavioural need that we would like our code to address. At this point we should be concentrating only on the public interface to our code. That is what we are designing at this point, nothing else.
If you are thinking about how you will implement this method or class, you are thinking of the wrong things. Instead, think only about how to write a nice clear test that captures just what you would like your code to do.
This is a great opportunity to design the public interface to your code. By focusing on making the test simple to write, it means that if ideas are easy to express in our test, they will also be easy to express when someone, even you in future, uses your code. What you are really doing, at the point when you strive for a simple, clear test, is designing a clean, simple to use, easy to understand API.
Treat this as a distinct, separate step from designing the internal workings of the code. Concentrate only on describing the desired behaviour in the test as clearly as you can.
Experienced TDD practitioners, like me, will tell you to do the simplest thing that makes the test pass. Even if that simple thing is trivial, or even naive. The reason that we advise this is because your code is currently broken, the test is failing. You are at an unstable point in the development.
If you start to try and do more complex things at this point, like make your design elegant or performant or more general, you can easily get lost and get stuck in a broken state for a while.
If the “simplest thing” is to return a hard-coded value, hard-code it!
This does a couple of things. It forces you to work in tiny steps, a good thing, and it also prompts you to write more tests that allow you to expand the logic of your code, another good thing.
Your tests should grow to form a “behavioural specification” for your code. Adopting the discipline of only writing production code when you have a failing test helps you to better elaborate and evolve that specification.
Don’t worry, we won’t forget to tidy-up the dumb, overly simplistic things that we do at this point.
Over-complicating the solution is one of the commonest mistakes that I see TDD beginners make. They try to capture too much in one step. They prefer to have fewer more complex tests than many, small, simple tests that prod and probe at the behaviour of their system. The small steps, in thinking and in code, help a lot. Don’t be afraid of many small simple tests.
Always refactor on a passing build. Wait until you are in the “GREEN” state before you begin. This keeps you honest and stops you wandering off into the weeds and getting lost! Make small simple steps and then re-run the tests to confirm that everything still works.
Refactoring is not just an afterthought, it is not just about aligning the indents and optimising the imports. This is an opportunity to think a bit more strategically about your design.
It is important that we treat it as a separate step. I often see things that I want to change either when writing a test (RED) or when writing code to make the test pass (GREEN). On my good days, I remember that this is not the time. I make a note and come back to it once the test is passing. On my bad days I often end up making mistakes, trying to do things in steps that are too big an complicated, rather than small and simple, and so I end up having to revert or at least think a lot harder than I need.
If you use a distributed VCS like GIT, I recommend that after each refactoring step, after you have checked that the tests all pass, commit the change. The code is working, and the committed version gives you a chance to step-back to a stable state if you wander-off into more complex changes by mistake.
In general, I tend to commit locally after each individual refactoring step, and push to origin/master after finishing refactoring, but before moving-on to the next test.
Another beginner mistake that I frequently observe is to skip the refactor step all together. This is a big mistake! The refactor step is the time to think a little bit more strategically. Pause and think about the direction in which your code is evolving, try and shape the code to match this direction. Look for the cues that tell you that your code is doing too much or is too tightly-coupled to surrounding code.
One of my driving principles in design is “separation of concerns” if your code is doing “something AND something else” it is wrong. If your code is doing a business level calculation and is responsible for storing the results – wrong! These are separate and distinct concerns. Tease out new classes, new abstractions that allow you to deal with concerns independently. This naturally leads you down the path towards more modular, more compose-able designs. Use the refactoring step to look for the little cues in your code that indicates these problems.
If the set-up of your tests is too complex, your code probably has poor separation of concerns and may be too tightly-coupled to other things. If you need to include too many other classes to test your code, perhaps your code is not very cohesive.
Practice a pause for refactoring every single time you have a passing test. Always look and reflect “could I do this better?” even if sometimes the answer is “no it is fine”.
The three phases of TDD are distinct and your mental focus should also be distinct to maximise the benefit of each phase.
Thank you for putting ot so clear and simple. Even appr. 30 years after TDD has been made public, it’s still not practiced by so many programmers…
The temptation to write larger unit tests, to write everything at the first instance, is big. It takes discipline to overcome it. My experience shows it works 🙂
Yes, I agree. It is a very common problem. The secret to TDD is that in many ways it is simpler than people think. Take small, even tiny, steps and your life becomes a lot easier.
The small tiny simple ateps are the key. You do small in/out experiments, smaller than the business domain process, until you solve the problem completly.
Hi Dave, I give similar/complimentary advice. I call it TDD guided by ZOMBIES in this blog article. http://blog.wingman-sw.com/archives/677
Nice article, Dave. Totally agree with you about commiting every baby step to VCS, it’s really helpful.
Anyway, how do you write a test for complicated scenarios? For example, a programmer needs to integrate with three different sources, request data from them, aggregate, sort, filter and things like that?
With TDD I can easily cover separately aggregation, filtering and sorting, but things going worse for a method that has to call all this business logic in one place. I mean there’s a business rule that firstly should be aggregate then filter and so on.
I don’t write this kind of test for “complicated scenarios”. The tests that I am talking about here are about very fast feedback and are meant to assert that the code does what the developer thinks it should do. Nothing more.
In my preferred approach, each of these tiny tests will be behaviourally focused, but within a very narrow context.
In the approach that I promote, these tests work alongside more broadly-scoped tests that we call “Acceptance Tests” these are “Whole system, black-box, tests from the perspective of an external user of the system. Testing life-like scenarios in a production-like test environment”. I wrote a bit about these kinds of tests…
and here: https://blog.xebialabs.com/2014/10/28/strategies-effective-acceptance-testing-part-ii/
I also have a conference presentation that goes into a bit more detail of how to implement these kinds of tests here: https://www.youtube.com/watch?v=I-fzJlH49G0
Pingback: Three Distinct Mind-sets in TDD | Dave Farley’s Weblog – Coding Nuggets & Trails
Good post, and I like to constantly be reminded of the routine for TDD.
Thank you for going back to the basics+beginning of TDD. There are many things I liked better in the early years of XP (from the late 90s to the early 2000s) before they were all umped together into a single heading of TDD, like the individual descriptions and techniques of test-first, DTSTTCPW (“Do the simplest thing …”), YAGNI, Refactor Mercilessly (’till you’ve said everything …), OAOO (“Once and Only Once”), and the four rules of simple code. (Much of that discussion took place on Ward’s Wiki back then, but also on the early days of the XP mail-list).
I also like that that they were later brought together under the banner of “TDD” – but I also lament that it resulted in losing the idea that TDD was a “composite” of multiple practices (including Test-First, Refactoring, and CI) that emerged from the synergistic combination those separate practices/techniques to form the discipline that is TDD.
I liked the “Red-Green-Refactor” mantra, but also what James Shore described as the critical but too often unspoken (and overlooked) initial step of “Think” (about what is the next test to write, what will best move the design incrementally forward). I also liked when some started referring to it as “Red-Green-Clean” (since it is more pithy, and rhyming).
Thank you for bringing all that back to the forefront.