Mon, 17 Jul 06

Test::Unit abstract test case

Having found a new way to structure my tests, the need for some common setup shared by multiple test cases became apparent.

I think the traditional way to do this was to create a subclass of TestCase to contain the common setup code and then subclass that for your individual test cases. This is fine, but in test/unit, a TestCase with no tests causes an error.

One solution would be to use a module to contain the common setup and include that in your test case.

module CommonSetup
  def setup
    @stack = Stack.new
  end
end
class MyTest < Test::Unit::TestCase
  include CommonSetup
  def test_should_accept_an_item_when_sent_push
    assert_nothing_raised { @stack.push(1) }
  end
end

Although this is fine, I spent a short while creating an AbstractTestCase that doesn’t complain if no tests are specified.

require 'test/unit'
class Test::Unit::AbstractTestCase < Test::Unit::TestCase
  def default_test
    klass = self.class.to_s
    ancestors = (self.class.ancestors - [self.class]).collect { |ancestor| ancestor.to_s }
    super unless klass =~ /AbstractTestCase/ or ancestors.first =~ /AbstractTestCase/
  end
end

Any test case (TestBase in this example) that inherits from AbstractTestCase can now contain setup and teardown methods but not have to worry about missing tests. In order to maintain expected behaviour, any test case subclassed from TestBase will still complain if tests are missing.

class TestBase < Test::Unit::AbstractTestCase
  # 'No test were specified' error is not thrown
  def setup
    @bar = { :foo => 123 }
  end
end
class RealTest < TestBase
  # Utilises TestBase#setup
  def test_example
    assert_equal 123, @bar[:foo]
  end
end
class ShouldErrorTest < TestBase
  # 'No tests were specified' error
end

I then figured that you may want more than one level of abstract test case, i.e. be able to define at the TestCase class level whether or not a test case was abstract. This isn’t properly tested but it seems to work ok.

Note This isn’t doing exactly what I thought (the default test case is default_test, not test_default). This works because it just creates an empty test in your test case. This breaks behaviour in test cases that subclass your abstract test case as they too will already have at least one test defined.

require 'test/unit'
class Test::Unit::TestCase
  class << self
    def abstract
      self.class_eval do
        def test_default
        end
      end
    end
  end
end

By sending the abstract message to any TestCase class, no errors will be reported when no tests are specified. It does this by overriding the test_default method; which is the method that raises the error if no other tests are specified.

class AbstractTest < Test::Unit::TestCase
  abstract
  # 'No test were specified' error is not thrown
end
class NormalTest < Test::Unit::TestCase
  # 'No test were specified' error
end