- Introduction
- Test-Driven Development
- Incremental Test-Driven Development
- The Payoff for Test-Driven Development
- Learning to Write Good Unit Tests
Incremental Test-Driven Development
When doing test-driven development, you're supposed to work in extremely small increments. For each method you write, start off with a unit test containing just one or two simple assertions, implement the method to make those assertions true, and then add more assertions to the unit test.
Ideally you want to be bouncing between adding assertions, revising the code, and running the tests every minute or so. Take very small steps and run the tests after every small change to make sure that you're heading in the right direction. After all, as Nathaniel Talbott, the author of Test::Unit, states, the assertions are the heart of any unit testing framework:
Think of an assertion as a statement of expected outcome, i.e. "I assert that x should be equal to y." If, when the assertion is executed, it turns out to be correct, nothing happens, and life is good. If, on the other hand, your assertion turns out to be false, an error is propagated with pertinent information so that you can go back and make your assertion succeed, and, once again, life is good.
To see this in practice, we can add another unit test to the TC_CircularCounter class, a test for the decrement method, with a single initial assertion:
def test_decrement @counter.decrement assert_equal(5, @counter.value, "Failed to decrement") end
Initially, obviously, Test::Unit will complain because the CircularCounter doesn't even have a decrement method:
>ruby tc_CircularCounter.rb Loaded suite tc_CircularCounter Started... .. Error occurred in test_decrement(TC_CircularCounter): NameError: undefined method ´decrement' for #<CircularCounter:0x2c2c290> tc_CircularCounter.rb:17:in ´test_decrement' tc_CircularCounter.rb:22 . Finished in 0.07 seconds. 2 runs, 4 assertions, 0 failures, 1 errors
But, as Talbott says, the fact that decrement is an undefined method is pertinent information. So we can go and add an empty decrement method to CircularCounter:
def decrement end
Now we get different information reported, but it's just as useful:
Failure occurred in test_decrement(TC_CircularCounter) [tc_CircularCounter.rb:18]: Should have decremented correctly. Expected <5> but was <6> . Finished in 0.04 seconds. 2 runs, 5 assertions, 1 failures, 0 errors
So we can go ahead and implement the method:
def decrement @value = @value - 1 end
Once again, Test::Unit will report that life is good. The time has come to make another assertion, stating that when the CircularCounter reaches zero the next decrement sets it back to the maximum value:
def test_decrement @counter.decrement assert_equal(5, @counter.value, "Failed to decrement") 6.times {@counter.decrement} assert_equal(7, @counter.value, "Failed to roll over") end
And once again our assertion turns out to be false. We have another failing test, so we can change the implementation of the decrement method until life is good again. (This is left as an exercise for the reader.)
Failure occurred in test_decrement(TC_CircularCounter) [tc_CircularCounter.rb:20]: Failed to roll over. Expected <7> but was <-1>