Test-Driven Development
Before you write any method in any class, figure out what the method is supposed to accomplish and then write a test to verify that the method does what it's supposed to do. Take for example the CircularCounter class from my last article. To test that class, we need the following Ruby code to be saved into a file called tc_CircularCounter.rb, which we can then run through the Test::Unit testing framework. (The set_up and tear_down methods are called by Test::Unit for every test case to make sure that each unit test starts off with known values.)
require 'test/unit' require 'CircularCounter' class TC_CircularCounter < Test::Unit::TestCase def set_up # sets up the initial values for each test @counter = CircularCounter.new(8,6) end def test_increment @counter.increment assert_equal(7, @counter.value, "Failed to increment") end def tear_down # does any necessary cleanup for each test @counter = nil end end
Running this code will demonstrate that we haven't yet implemented the methods:
>ruby tc_CircularCounter.rb tc_CircularCounter.rb:2:in ´require': No such file to load -- CircularCounter (LoadError) from tc_CircularCounter.rb:2 Loaded suite tc_CircularCounter Started... Finished in 0.01 seconds. 0 runs, 0 assertions, 0 failures, 0 errors
No real surprises there. It can't load the CircularCounter file because it hasn't been written yet. No problem; just grab the definition from last time and save it into CircularCounter.rb.
class CircularCounter def initialize (limit, initialValue) @limit, @value = limit, initialValue end def value @value #could use 'return @value' to be more explicit end def reset @value = 0 end def increment # returns true if overflow occurred @value = @value + 1 if (value >= @limit) reset true else false end end end
Rerunning the unit test shows that the CircularCounter class is now found and the assertion we made that the counter would be incremented turns out to be true:
>ruby tc_CircularCounter.rb Loaded suite tc_CircularCounter Started... .. Finished in 0.01 seconds. 1 runs, 1 assertions, 0 failures, 0 errors
To make sure that the counter rolls over the limit correctly, we should add another assertion to the test_increment method:
def test_increment @counter.increment assert_equal(7, @counter.value, "Failed to increment") 7.times {@counter.increment} assert_equal(6, @counter.value, "Failed to roll over") end
Running the unit test will then show that both assertions are true:
1 runs, 2 assertions, 0 failures, 0 errors
Since this is looking rather boring, let's introduce a deliberate (though easy to make) mistake. Say, for example, we had written this:
if (value > @limit)
rather than the correct entry:
if (value >= @limit)
The unit tests would have complained with the following message:
>ruby tc_CircularCounter.rb Loaded suite tc_CircularCounter Started... .. Failure occurred in test_increment(TC_CircularCounter) [tc_CircularCounter.rb:12]: Failed to roll over. Expected <6> but was <5> Finished in 0.05 seconds. 1 runs, 2 assertions, 1 failures, 0 errors
Test::Unit reports the failing assertion, telling us the line number of the failing assert, printing out our error message Failed to roll over, and it prints both the value we expected to see and the actual value. Given all of that information, the mistake is usually very easy to seeif it isn't immediately obvious, either get someone else to have a look or add more assertions to the test code. In our case, we can exploit the fact that on rollover the increment method is supposed to return true. (We also have to reduce the number of times we call increment in the loop because we're also calling it twice outside the loop.)
def test_increment @counter.increment assert_equal(7, @counter.value, "Failed to increment") assert(@counter.increment, "Should have rolled over") assert(!@counter.increment, "Should not have rolled over") 5.times {@counter.increment} assert_equal(6, @counter.value, "Failed to roll over") end
With this extra assertion, Test::Unit will now report that the CircularCounter failed to roll over, giving an even stronger hint about the (deliberate) mistake:
Failure occurred in test_increment(TC_CircularCounter) [tc_CircularCounter.rb:11]: Should have rolled over