Sunday, June 17, 2007

DRYing up Unit Test Preconditions and Postconditions

It's good practice in unit testing to assert any preconditions (aka assumptions) relevant to the test and the expected postconditions (aka side-effects). I had a unit test that went something like this:
class TaggableTest < Test::Unit::TestCase
  fixtures :tags, :items
  def test_tagging
    one = items(:one)
    one.tag_list = "tag1, tag2, tag3, tag4"
    assert_equal 0, one.tags.size
    assert_equal 3, Tag.count
    assert_equal 0, Tagging.count
    assert_equal 4, Tagging.count
    assert_equal 4, Tag.count
    assert_equal 4, one.tags.size
I didn't care for this code because of the redundancy of the assert equals. So I added some new asserts to TestCase in test_helper.rb and was able to convert it into the following:
  def test_tagging
    one = items(:one)
    one.tag_list = "tag1, tag2, tag3, test4"
    assert_changed(lambda {one.tags.size},
                   lambda {Tag.count},
                   lambda {Tagging.count},
                   :from => [2, 3, 2],
                   :to => [4,4,4]) { } 
assert_changed will execute the code blocks passed in prior to the execution of the primary block and again afterwards. It will then compare those values to the from and to values (optionally) passed in. If you don't pass in both :from and :to, assert_changed merely validates that the values changed.
  def singleton_maybe(s)
    return s[0] if s.is_a?(Array) and s.size == 1
    return s
  def assert_array_or_svo_equal(expected, actual)
    expected = singleton_maybe(expected)
    actual = singleton_maybe(actual)
    assert_equal expected, actual

  def assert_changed(*expressions)
    options = {}
    options.update(expressions.pop) if expressions.last.is_a?(Hash)
    initial = {|e|}
    assert_array_or_svo_equal options[:from], initial if options.has_key?(:from)
    subsequent = {|e|}
    initial.each_with_index { |i, n| assert_not_equal i, subsequent[n] } if not (options.has_key?(:to) and options.has_key?(:from))
    assert_array_or_svo_equal options[:to], subsequent if options.has_key?(:to)
  def assert_unchanged(*expressions)
    initial = {|e|}
    subsequent = {|e|}
    initial.each_with_index { |i, n| assert_equal subsequent[n] }

No comments: