Wednesday, June 06, 2007

Testing: One assertion per test

Limiting your tests to using one assertion is a controversial topic. I originally stumbled upon the idea on Dave Astels' blog. I liked the style of development that Dave described and decided to give it a try, that was over 2 years ago. Since then I've worked on teams ranging from 4 developers to 16, codebases in Ruby and C#, and project timelines ranging from 3 months to 8. I think it's fair to say I've given the concept plenty of chances to fall down. But, regardless of the variables, the guideline has always remained valuable.

For me, the main motivator for using one assertion per test is the resulting maintainability of the test. Tests that focus on one behavior of the system are almost always easier to write and to comprehend at a later date. I've always been better at understanding through examples, so let's take a look at some tests written to test the PhoneNumber class.

class PhoneNumber
attr_accessor :area_code, :exchange, :station

def initialize(area_code, exchange, station)
@area_code, @exchange, @station = area_code, exchange, station
end
end

class PhoneNumberTest < Test::Unit::TestCase
def test_initialize
number = PhoneNumber.new "212", "555", "1212"
assert_equal "212", number.area_code
assert_equal "555", number.exchange
assert_equal "1212", number.station
end
end

The above code works, but if the PhoneNumber class contained a bug in the initialize method, only the first failing assertion would be reported.

class PhoneNumber
attr_accessor :area_code, :exchange, :station

def initialize(area_code, exchange, station)
area_code, exchange, station = area_code, exchange, station
end
end

class PhoneNumberTest < Test::Unit::TestCase
def test_initialize
number = PhoneNumber.new "212", "555", "1212"
assert_equal "212", number.area_code
assert_equal "555", number.exchange
assert_equal "1212", number.station
end
end

# >> Loaded suite -
# >> Started
# >> F
# >> Finished in 0.006025 seconds.
# >>
# >> 1) Failure:
# >> test_initialize(PhoneNumberTest) [-:14]:
# >> <"212"> expected but was
# >> <nil>.
# >>
# >> 1 tests, 1 assertions, 1 failures, 0 errors

This is the first reason I dislike multiple asserts in one test. In this example it would be easy to notice that all three variables are set incorrectly; however, more often fixing the first failing assertion only leads to finding out what's wrong with the 2nd assertion. I'd rather know the first time I run the suite that 10 things are failing, not that 5 are failing and a few others may or may not be failing.

Another reason I dislike multiple assertions is that it's hard to give a descriptive name if you are testing various behaviors. For example, the error message test_initialize(PhoneNumberTest) [-:14]: <"212"> expected but was <nil> isn't the most descriptive in the world. You can argue that I didn't name my test correctly; however, the test_area_code_exchange_and_station_are_initialized_correctly test doesn't tell me much either. On the other hand, the test_area_code_is_initialized_correctly test tells me exactly what behavior I'm testing (or what behavior is currently wrong when a test fails).

require 'test/unit'

class PhoneNumber
attr_accessor :area_code, :exchange, :station

def initialize(area_code, exchange, station)
area_code, exchange, station = area_code, exchange, station
end
end

class PhoneNumberTest < Test::Unit::TestCase
def test_area_code_is_initialized_correctly
number = PhoneNumber.new "212", "555", "1212"
assert_equal "212", number.area_code
end

def test_exchage_is_initialized_correctly
number = PhoneNumber.new "212", "555", "1212"
assert_equal "555", number.exchange
end

def test_station_is_initialized_correctly
number = PhoneNumber.new "212", "555", "1212"
assert_equal "1212", number.station
end
end

# >> Loaded suite -
# >> Started
# >> FFF
# >> Finished in 0.01048 seconds.
# >>
# >> 1) Failure:
# >> test_area_code_is_initialized_correctly(PhoneNumberTest) [-:14]:
# >> <"212"> expected but was
# >> <nil>.
# >>
# >> 2) Failure:
# >> test_exchage_is_initialized_correctly(PhoneNumberTest) [-:19]:
# >> <"555"> expected but was
# >> <nil>.
# >>
# >> 3) Failure:
# >> test_station_is_initialized_correctly(PhoneNumberTest) [-:24]:
# >> <"1212"> expected but was
# >> <nil>.
# >>
# >> 3 tests, 3 assertions, 3 failures, 0 errors

Testing this way also helps me think critically about my domain model. If I aspire to write tests that contain only one assertion, often the methods of my domain model end up with a single responsibility.
Post a Comment