Home > Articles > Programming > Ruby

  • Print
  • + Share This
From the author of

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 see—if 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
  • + Share This
  • 🔖 Save To Your Account