Tuesday, November 24, 2009

Conditionals in Unit Tests

One of the questions newcomers to TDD (Test-Driven Development) often ask is: how can I trust test code to be correct?

Well, the reality is that it is not black and white. Not every instance of implementation code is prone to bugs (think getters and setters) and not every instance of test code is perfectly free of bugs. However, as software engineers, we are more concerned with the practical aspects of software development, and experience seems to indicate that if you write your test code in a linear fashion without using conditionals, then it is less prone to having bugs, and thus can serve as a useful tool in driving reliable implementation code as per the requirements specified.

Back to the question: how can I trust test code to be correct?

Test code often follows this structure:
  • Pre-conditions setup
  • Action being tested
  • Post-condition verification
For example (in Ruby):

# pre-conditions
time = Time.new

# action
hours = time.hours_between(9am, 2pm)

# post-conditions (specified with RSpec)
hours.should == [9am, 10am, 11am, 12pm, 1pm, 2pm]

Since that code is linear and free of conditionals, if it parses successfully, it generally expresses what it says without much ambiguity and thus has very little chance for error.

Now, let's look at a version of the implementation after a few tests have been written:

def hours_between(start_time, end_time)
  (numeric_time(start_time) .. numeric_time(end_time)).map do |numeric_time|
    textual_time(numeric_time)
  end
end

def numeric_time(time)
  meridian_indicator = time[-2..-1]
  numeric_time = time.delete(meridian_indicator).to_i
  numeric_time = meridian_indicator == "am" ? numeric_time : numeric_time + 12
  numeric_time = 0 if numeric_time == 24
  numeric_time
end

def textual_time(numeric_time)
  meridian_indicator = numeric_time < 12 ? "am" : "pm"
  textual_time= numeric_time < 12 ? numeric_time.to_s : (numeric_time - 12).to_s
  textual_time= "12am" if textual_time == "0am"
  textual_time+ meridian_indicator
end

Do you notice how much complexity there is with reading statements that involve conditionals. It's doable, but definitely takes work despite how factored the code is.

Note that the implemented functionality is not entirely correct as it only works if the range specified is between 1am and 11pm. More tests need to be written to drive the rest of the implementation. However, given that tests do not have any conditionals, they provide us with an automated way of testing that our implementation works according to plan.

So, avoid conditionals in unit tests, and you will benefit from them in implementing more reliable code.

1 comment:

kevin Taylor said...

Hi Andy,

Yes, good points. It takes a bit of experience before you realize that it is best to just keep your tests simple and linear.

Otherwise, so many negative issues may arise in your test suite. One, which you so well illustrated, is that your tests may end up with their own bugs.