In TDD, there are three steps to making a code change:
- Write a test.
- Make it pass.
I sometimes find myself skipping the third step. Not because I don’t like clean code, but because I combined it with making the test pass. To help explain why I don’t like doing that, and how I avoid it, here’s a more elaborate explanation of the steps as I try to practice them:
- Write a test first. Watch it fail.
- Fix only that failure the simplest way you can. Repeat until the test passes.
- Clean up the mess made in step 2.
The “Make it pass” step is not “write the fully-designed behavior I’m trying to drive with this test”. I want to get rid of that red failure message quickly so I can work in the green where it’s safe. I don’t care (too much) about DRY or SOLID or any of that yet. I’ll worry about that in step three when I have a passing test to act as a safety net. Right now I’m in the danger zone. I need to get out by any means necessary.
Is it failing because the
get_price function I’m testing doesn’t exist yet? I write the function definition and leave it empty. Is it failing because the return value doesn’t equal 9.99? I return a literal 9.99 from the function. I’m green! I can move to step three and clean up my mess now that my safety net is in place.
Kent Beck calls this step “remove duplication”. I have a magic value of 9.99 in both my test code and my implementation. With TDD, I want my tests to become more specific as my implementation becomes more general, so I’ll remove the duplication by changing my implementation from the hard-coded solution to a general abstract solution. Maybe I can get 9.99 by returning a variable, an attribute on an object, or maybe the refactoring needed is bigger than that, or maybe I can’t see what refactoring is needed yet.
In the case of a refactoring that I can’t see, I might decide to live with the duplication for now, and write another test to drive it forward, asserting some other input’s price is 5.99. I’ll repeat this until a refactoring becomes clear.
If I can see the refactoring, but it will require changing more than a couple lines, I don’t try to do it all at once. Instead, I work backwards, starting with a call to a final implementation that I wish I had. Maybe returning
product.price. This puts me back into the red because that doesn’t exist yet. Now I drop back into a TDD loop (the process is recursive): I have a failing test, so fix it by any means necessary. Maybe I just move that magic value to the price attribute. Now clean that up. Repeat until I’m satisfied with the implementation.
In all cases, I’m never taking too large of a step. Ideally, I’m one or two undos away from a working state. J. B. Rainsberger calls theses micro steps. I can’t always do this, but it’s the goal I strive for because it’s when I’m most comfortable. I’m terrible at keeping more than one thing in my head at a time. I hate when an interruption derails an hour of work because there was too much up there and it all fell out. When every change I’m making feels boring, I’m happy. That doesn’t mean I never step back and consider the big picture – you’re not doing your overall design any favors by only focusing on the micro scale – it means I don’t have to do that while I’m also trying to get code working. Remembering that refactoring is its own separate step is the best way I’ve found to enable that.
The refactoring step isn’t important to me simply for the sake of clean code, but because its existence means I don’t have to do too much at once. I don’t have to worry about clean code while I’m trying to get something working, and I don’t have to worry about getting something working while I’m trying to clean the code.