I always tell people: When you need to add a new feature, use the TDD way. Write a test first, make small steps, and all will be well.
But sometimes, we need to take other paths.
I was working on a new feature last week. I already had a test in place, and a couple more to follow. I usually write those in comments, so I won’t forget to make sure they work, or add the code.
And so I start coding. And move code around. And refactor here and there. And low and behold, after two hours, I’m still in the same place. That test still doesn’t pass. I took a step back, slept on it, and reverted to the original passing code.
Job too big for one test
I knew where I was going (better than when I started the first time). So while all tests were green, I started to refactor the code, putting placeholders for the new code. This is similar to coding by intention, or a variant of “fake it till you make it”.
Then I went to the test side. I thought about going with TDD for a more internal component, but the way the architecture was built, it didn’t make sense to me. Plus, the original test described the behavior I wanted. I kept the original test.
Next, I went back to the code. I had some placeholders to fill. I knew that some of the code will converge into the same methods. But I was careful not to touch the existing code. I only added code, duplicated (sometimes triplicated) it. I knew that there will be time for refactoring when everything works. I kept running the tests to make sure the old tests still work.
After an hour or so, I had most of the code in place, and still, a failing test. Just filling the placeholders was not enough. I could have reset and start over. But I decided to push through. Half an hour later the test was passing. A few minutes later the additional tests were passing, without adding more code. An hour later, the code was refactored to my satisfaction.
What lessons can we learn?
- TDD tells us to go in small steps. That is, small working increments of code. But sometimes the jump is too big. Real life is hard.
- Doing a once-over, even if not completed, did help. It showed me the “design of things to come”.
- Throwing away the changes helped, and didn’t feel wasteful. I was no longer bound to my changes from the first session.
- Working without a net (while not all tests are passing) felt weird (even scary). Going in the second time, I already knew it won’t be easy. But having the placeholders helped me feel I’m on the right path, as I was making them green.
- Knowing there’s going to be refactoring later also helped. I concentrated on “making it work”, rather than the nagging “how it looks” feeling. I did fight the urge to consolidate code that looked similar, but prevailed.
- At the end I was happy. The feature worked, I had passing tests, and the code looked better (although not exactly as I envisioned).
So what is the moral of the story? TDD is still my favorite way of coding. But like every tool, it’s not the only one I can use. In reality, It’s not the hammer for every nail out there.
2 Comments
Arnon Axelrod · August 17, 2020 at 7:46 am
IIUC, you did start with a new failing test that described the behavior you wanted, and eventually you had this test, plus all previous tests, working. And clearly, along the way you refactor the code.
So why isn’t that TDD? just because it was somewhat bigger stops than you’re used to?
Gil Zilberfeld · August 19, 2020 at 9:08 am
Technically that’s test first. TDD is built on the small increments, and adding just the code that works. In my case that was a very VERY big step. When you’re doing the big steps, that means you very quickly move from “just the code that works” to “code I think will make it work”. That has a big effect on confidence, YAGNI, focus and other stuff.
There are effects to each method. And finally, that it’s not TDD doesn’t make it bad, it got me where I wanted to be. The road was a bit bumpy.
I’m sure you’ve taken paths that seemed not the best to take but got you where you wanted, right? I’ll be happy if you can share.