Monday, December 28, 2009

TDD thoughts / notes after reading Kent Beck's TDD By Example

I've read James Newkirk's (the original developer of NUnit) book on TDD, been to TDD workshops (including one with James Newkirk), and consider myself to know a fair amount about TDD. I drank the kool-aid several years ago and have been trying to preach the good news ever since. I hadn't, however, read Kent Beck's Test-Driven Development By Example until today. I would honestly say that this is the best software book I've ever read, and it only took me a few hours to get through. It is simple and easy to understand, even when covering fairly complicated material. Bravo. The notes that follow are based on the book. They are not a book report, but a reminder for me and others to follow when trying to fully grok TDD.
--------
What is our programming / development style? In other words, how do we typically implement and verify business (i.e. non-UI) rules? 5 options I’ve seen / done:
  1. Modify existing function or add a new one then open a web browser, click a button, then verify on the web page
  2. Modify existing function or add a new one then open a web browser, click a button, then verify in the database (similar to using SQL Profiler)
  3. Add Response.Write, STOP, etc. statements in code, or add CSS around an element to call attention to it and go from there
  4. Write a bunch of new classes / methods / etc. in an effort to finish a task before a deadline, then come back to them weeks later and write some xUnit tests to boost code coverage from zero to x…. only to find out when writing your tests that your design / architecture is far from ideal. aka TLIE (Test Last, If Ever)
  5. Open xUnit, create an automated test, make it pass, and then change the necessary code base, cry tears of joy… aka TDD
Which of those (and others you can think of)…
  • is repeatable?
  • leaves a signpost for future developers, documenting that a certain feature exists?
  • provides an example API usage for future developers (including myself in that audience)?
  • is fast? (and therefore gives you the ability to try several implementations when you have a few options in mind and aren’t sure which not only works but is best)
  • is isolated (can happen even if everything else is broken)?
  • does not require (much, if any) human work?
  • gives us confidence when making any change, no matter the size?
Testing changes is not the same as having tests.

Why is writing the automated test important at all?
  • Automated tests help us verify that we definitely know what we think we know. Adding more helps convert “what we don’t know” to “what we know” …. err, something like that.
  • Tests provide living and up-to-date documentation in source control.
  • Especially in our code base, changing almost anything is stressful. Automated tests are stress management tools. Passing automated tests eliminate the stress of “what we don’t know” and gives us the courage to focus on our real passion, which is adding more value to our customers. [If you feel no stress at all when debating changing code in your current production codebase, please leave the room after reading the following out loud: I, _________, apologize to the entire company for the future hotfixes I am about to create a need for. I apologize to our production release team, to support, my fellow developers, and to our customers. I have stocked the kitchen with Red Bull, Tylenol, and Prozac. I will begin working on handwritten letters of apology immediately.]
  • “In TDD, the tests are a means to an end – the end being code in which we have great confidence.”
  • You tell me
2 simple tenants of TDD:
  1. Write new code only if an automated test has failed
  2. Eliminate duplication
So how do we do that?
  1. Red
    • Write the test first to tell the story of what we wish to implement,
    • how we wish our API to look (Beck calls this “the perfect interface for our operation” and tells us to “Invent the interface you wish you had.”),
    • what will cause the test to pass (assertions),
      • Write the assert statements first so you know when you are done.
    • help define clearly what is in scope and what is not.
    • This gives us a concrete measure of failure!
  2. Green
    • Fix the failing test to pass to know with certainty the given story has been completed
    • Even if it’s ugly, just focus on turning the red bar to green fast
    • This gives us a concrete measure of success!
  3. Refactor
    • Keep a tidy house and make sure the green bar still exists afterward
    • If you used constants to get a single test to pass in step 2, add another assertion and generalize the code to work for all scenarios (triangulation).
    • Change the code but not the outcome or business rules. This is a stressful and time-consuming exercise unless you have tests as a safety net to guarantee even massive code changes don’t cause a change the in code’s behavior as the end user sees it.
What other subtle thing does this mean we’re always doing?
  • Design. TDD is continual design. Design is fluid and never finished. Features (and scope) are complete when all tests pass, but design goes on like the Energizer Bunny. Do not be surprised to see your design veer wa(aaaaaa)y off course from what you originally thought you should originally end up with.
Why is writing the test first important?
  • Design is mandatory instead of an afterthought
  • You start with the world’s greatest API for the given problem and only back down from that when necessary
  • Tests are a way to identify scope boundaries and help us stay within them while we develop. We isolate a tiny low-stress situation, and only move outside of that scope when we are ready to write a new test. When we have a failing test that we need to make a passing test, we cannot ourselves to get sidetracked by, “oh yeah, I also need to implement this for this project…” and then we’re 30 minutes later before we remember what we were working on. In this way, writing tests first eliminates waste. (!!!!!)
The pattern for writing new tests
  1. Arrange – create some objects (setup your condition)
  2. Act – stimulate them (force the situation under test to execute)
  3. Assert – check the results
I have seen this called AAA Syntax. (See Rhino Mocks)

Before beginning any task, write a list of all the tests you know you will have to write. After you have Red>Green>Refactored a story off of your to-do list, select the next item from the list based on:
  • level of confidence (am I certain I can make this work?)
  • will this teach me something?
If you discover a potential problem, do not be afraid to simply add it to the to-do list instead of interrupting the current Red>Green>Refactor session.

Some things you might already know but need to reconsider:
  1. Make sure your tests are isolated. One failing test should not cause 3 others to fail. The order in which the tests run should also not matter.
  2. Change the voice inside your head from “how do I implement feature X” to “how do I test my implementation of feature X” any time you open the IDE and consider typing.
  3. You decide your velocity with TDD. If you find yourself getting annoyed at running tests after changing every single line of code that you know will not break a test (such as changing a variable name inside a 4 line c# method) then allow yourself to make bigger incremental changes before re-running tests. If you find yourself stressing over whether all your tests will pass or not, slow down. Redline the Bronco like OJ on a clear sunny day and slow down when the road gets icy.
  4. Every time you have a test fail unexpectedly, write a new test to document what was just discovered. It may mean you just found a bug.
    • Along the same lines, take the time to discover why something works when you think it shouldn’t.
    • Along the same lines, if you find a refactoring which your current tests do not demand, write another test.
  5. Your test code may actually be as many lines as the code you are adding. This is not a bad thing.
    • Doesn’t this mean my output is going down? When answering that question, be sure to take debugging time, integration, and code explanation / learning into consideration. (Just because something makes sense to you doesn’t mean it will be easy for someone else to learn… or even for you to recall in the future when adding feature Y to the codebase built for feature X. Your tests at least document your obvious-now-but-likely-to-become-confusing-later solution.)
  6. Do not be afraid to delete assertions, tests, or methods as part of refactoring; however, be confident that a test is obsolete before deleting. If the test does not give you any more confidence by existing, and it doesn’t communicate anything new to the reader of the test that they can’t learn in another test, it can be deleted.
  7. TDD does not in any way replace or eliminate a need for performance / stress / usability testing.
  8. 100% test coverage does not mean 0 bugs.
  9. Can we change the meaning of 1 line of code and then have a broken build? If not, we need another test. In other words, if we change line 873 (umm, first of all, we should never have a line 873, but that's a different discussion...) of ____.cs to say x = true instead of x = false, we should have a failing test.
Lastly, do not forget that changing code without a failing test is a violation of tenant #1 (Write new code only if an automated test has failed).

I think one of the most potentially difficult things about TDD is to let yourself solve the problem simply and then make it as pretty as you want to. When you solve it simply with passing tests, you can then change the solution to be reusable, decked out with patterns, etc. and be confident that the more complicated solution still solves the problem. In other words, don’t worry about it being pretty right out of the gate. Worry about solving the problem by making the test pass, and then let the patterns or perfection reveal itself through design changes, aka refactoring. Beck says this specifically in reference to the Template Method pattern, “Template methods are best found through experience instead of designed that way from the beginning.” but I think it applies to all (err, most?) patterns. He later states that extracting too many methods can lead to the reverse (inlining methods) when you think start off with a Template Method instead of following the above advice… so save yourself the time and let the patterns reveal themselves instead of creating patterns for the sake of patterns.