Test-Driven Development
I won’t get preachy about this, I promise.
Apparently one of the topics of discussion at Pablo’s Fiesta was whether TDD is a fad.
As a kind of response to the question “is TDD a fad”, let me focus on something everyone likes to talk about, and that is me. Me me me me me. Not you--me.
My story before Test-driven development
It’s college. I’m learning about object-oriented programming and have a pretty firm grasp. I can make classes, methods, static methods, and even make the right decision as to whether to go with a struct rather than a class*. I even know about singletons.
*"struct vs class" – in case you're wondering, the answer is "always class, unless you're
Unfortunately, my compiler project is a complete mess. I use the same data structure (let’s call it a class) for each stage of the compilation process. I sit frozen at the keyboard, sometimes with a piece of paper and a box-and-arrows-looking diagram, sometimes just sitting slack-jawed staring through the wall behind the monitor, trying to figure out where to put that behavior I need to implement.
It’s slow going. It’s a lot of rewriting. There’s dead code everywhere, some of which I know about, some of which I don’t. I try to map out everything I need to get this working, and stub out some of the methods I will need later. Sometimes I forget what I’m doing mid-step and just…blank out.
It’s a bad time.
My story from last Friday*, at work
* Last Friday…in September. Through the magic of first forgetting, then rediscovering this draft, I am able to traverse time itself.
First, I read the user story to make sure I knew what I was supposed to do. Something about adding another bit of our application to be searchable. Check. Once I had a vague idea of what to do, the first thing I did was write a test that spans all the way from a SearchViewModel down to the database (and yes I said database. It’s a simple search, we’re not using Lucene or anything crazier, lay off me). Specifically, I wrote some code to a) create an entity, b) save the entity to the database from whence it will be searched, c) get me a SearchViewModel in as similar a fashion as possible to our WPF-based UI, d) ran the search, and e) inspected the ViewModel for the search results I expected.
With this large (and yes, slow) test harness supporting me, I went on to implement the search functionality.
An aside
Let me take a moment to talk about a few things I didn't test. I didn’t write a unit test for every interaction within my own internal search API. I didn’t write a test from the ViewModel that mocked out the search service (Searcher) to test interactions. In my test, I didn’t even inspect properties of the search results to ensure that I’m getting the right search results, just lazily counted how many search results come back. See, I already have tests that verify each and every property coming back from search results, so why would I bother checking every property in every test? Anyway. "What to test" is the subject for another day, and no, I don't have the final answer either.
Back to my last Friday
After implementing enough code to make the first test pass, I ran the UI on my machine and verified that searching did everything it was supposed to. No problems this time—wait, I somehow mismatched two properties—oops, need to fix that. Went back (without writing a test or adjusting a test to test for the bug I just identified), made the necessary code change, fired up the UI again and inspected.
After I verified the code was in a stable state, I went back looking for things to clean up. No methods to rename, no dead or “temporarily commented-out” code, no code sitting in the wrong class. This time. So no refactoring work needed.
Another side note
Most TDD instructions will tell you to only implement the bare minimum needed to make a test pass. This is good contextual advice, given that the vast majority of developers create "speculative" methods and functionality and need to learn how to do truly emergent design (also known as design-by-example, or, the "YAGNI-You Ain't Gonna Need It" principle of design) via TDD.
But, we are also told that it's okay to do a some up-front design. Depending on who you heard it from, sometimes you hash out a class diagram that fits on a napkin (then as they famously say, throw the napkin away). Or, you can use CRC cards or Responsibility Driven Development(?), and Spiking, all of which, even if you don't know what they mean, sound like they involve doing something above the bare minimum needed to make a test pass. Anyway. Kent Beck's TDD book even tells you Triangulation is only one of the approaches to making a test pass, another approach being "just write the code you actually want in your finished product AKA the Obvious Implementation".
Okay. So here we are. We have the same people telling you you should do a) practice "pure" TDD by doing zero up-front design, and then b) telling you all these other things that directly contradict a). I've personally reconciled the conflicting advice as follows:
- Practice YAGNI. Speculative design tends to be bad, and from what Resharper tells me, the rest of you leave a lot of completely and obviously dead code lying around, not to mention all the extra unused public methods and classes you can find with FxCop or Resharper's "Solution-wide analysis". You guys, you guys.
- But do spend some amount of time thinking ahead, and maybe just implement the code you want to end up with, not strictly the bare minimum needed to make a test pass. If you have an Add(a, b) method, instead of writing 50 tests, and triangulating towards "return a+b", you're allowed to write a+b the first time and write enough tests to catch all scenarios. Triangulation helps keep my mental load down so I can keep moving towards solving the problem, and because I often find that having solved the problem through Triangulation, I have followed YAGNI and an unexpected design has "emerged".
- If you're not sure if you should follow #1 or whether you are allowed to cheat and follow #2, well, follow #1. Don't be "pragmatic" and use the word "pragmatic" as the blanket you use to justify whatever you want to justify.
If the YAGNI style of programming is new to you, the amount of code you'll rewrite while attempting it at first will be staggering, but through the pain, after rewriting everything about 5 times and deleting 2/3rds of your codebase, you'll figure out that you haven't really been applying YAGNI. And then you'll rewrite everything a few more times.
I'm probably sounding a little preachy, so let me be clear: I'm talking to myself from a few years ago. And I'll probably come back some years later and yell at myself for dismissing the relative value of unit tests versus integration tests. Yes, see you in 2020 for the follow-up post: "Peter is wrong: Again. Part 17 and counting".
Back to last Friday (again)
I run the tests (I have about twenty by the time I'm totally finished), do a quick diff on the files I have checked out, check in, and verify the build remains green. Our build compiles and runs all tests in about ~30 minutes. It’s slow. And no, I didn't check in every time the tests turned green (though I do shelve frequently). This paragraph sponsored by twitter hashtag #realtalk. #hashtagsInBlogPosts #weUseTfs
A world of difference
Being able to take a vague idea of what I need to do and steadily translate bits of requirements into working code is a world of difference from me in college. I haven't reached the summit—I probably shouldn't have to write the phrase "I haven't reached the summit", but just in case: "I haven't reached the summit. No comments please."
And, back to the original question: is TDD a fad? As for me:
Learning (and applying) Test-Driven Development has improved my programming ability more than than anything else, by far.
So to answer the question: hey. Hey. Let's be pragmatic and not get too carried away. Maybe it is a fad. It's good to be pragmatic, not too far on the left, not too far on the right, but somewhere in between in the pragmatic zone (the region where pragmatism reigns). Pragmatism is delicious when spread evenly over toasted bread and served with tea. If someone is drowning, be pragmatic about the situation. If you had to choose between having a tail or the gift of flight, be pragmatic.