views:

3001

answers:

8

I'm struggling with Test::Unit. When I think of unit tests, I think of one simple test per file. But in Ruby's framework, I must instead write:

class MyTest < Test::Unit::TestCase 
   def setup 
   end

   def test_1 
   end

   def test_1 
   end
end

But setup and teardown run for every invocation of a test_* method. This is exactly what I don't want. Rather, I want a setup method that runs just once for the whole class. But I can't seem to write my own initialize() without breaking TestCase's initialize.

Is that possible? Or am I making this hopelessly complicated?

+5  A: 

That's how it's supposed to work!

Each test should be completely isolated from the rest, so the setup and tear_down methods are executed once for every test-case. There are cases, however, when you might want more control over the execution flow. Then you can group the test-cases in suites.

In your case you could write something like the following:

require 'test/unit'
require 'test/unit/ui/console/testrunner'

class TestDecorator < Test::Unit::TestSuite

  def initialize(test_case_class)
    super
    self << test_case_class.suite
  end

  def run(result, &progress_block)
    setup_suite
    begin
      super(result, &progress_block)      
    ensure
      tear_down_suite
    end
  end

end

class MyTestCase < Test::Unit::TestCase

  def test_1
    puts "test_1"
    assert_equal(1, 1)
  end

  def test_2
    puts "test_2"
    assert_equal(2, 2)
  end

end

class MySuite < TestDecorator

  def setup_suite
    puts "setup_suite"
  end

  def tear_down_suite
    puts "tear_down_suite"
  end

end

Test::Unit::UI::Console::TestRunner.run(MySuite.new(MyTestCase))

The TestDecorator defines a special suite which provides a setup and tear_down method which run only once before and after the running of the set of test-cases it contains.

The drawback of this is that you need to tell Test::Unit how to run the tests in the unit. In the event your unit contains many test-cases and you need a decorator for only one of them you'll need something like this:

require 'test/unit'
require 'test/unit/ui/console/testrunner'

class TestDecorator < Test::Unit::TestSuite

  def initialize(test_case_class)
    super
    self << test_case_class.suite
  end

  def run(result, &progress_block)
    setup_suite
    begin
      super(result, &progress_block)      
    ensure
      tear_down_suite
    end
  end

end

class MyTestCase < Test::Unit::TestCase

  def test_1
    puts "test_1"
    assert_equal(1, 1)
  end

  def test_2
    puts "test_2"
    assert_equal(2, 2)
  end

end

class MySuite < TestDecorator

  def setup_suite
    puts "setup_suite"
  end

  def tear_down_suite
    puts "tear_down_suite"
  end

end

class AnotherTestCase < Test::Unit::TestCase

  def test_a
    puts "test_a"
    assert_equal("a", "a")
  end

end

class Tests

  def self.suite
    suite = Test::Unit::TestSuite.new
    suite << MySuite.new(MyTestCase)
    suite << AnotherTestCase.suite
    suite
  end

end

Test::Unit::UI::Console::TestRunner.run(Tests.suite)

The Test::Unit documentation documentation provides a good explanation on how suites work.

Romulo A. Ceccon
+1  A: 

I came across this exact problem and created a subclass of Test::Unit::TestCase for doing exactly what you describe.

Here's what I came up with. It provides it's own setup and teardown methods that count the number of methods in the class that begin with 'test'. On the first call to setup it calls global_setup and on the last call to teardown it calls global_teardown

class ImprovedUnitTestCase < Test::Unit::TestCase
  cattr_accessor :expected_test_count

  def self.global_setup; end
  def self.global_teardown; end    

  def teardown
    if((self.class.expected_test_count-=1) == 0)
      self.class.global_teardown
    end
  end
  def setup
    cls = self.class

    if(not cls.expected_test_count)
      cls.expected_test_count = (cls.instance_methods.reject{|method| method[0..3] != 'test'}).length
      cls.global_setup
    end
  end
end

Create your test cases like this:

class TestSomething < ImprovedUnitTestCase
  def self.global_setup
    puts 'global_setup is only run once at the beginning'
  end

  def self.global_teardown
    puts 'global_teardown is only run once at the end'
  end

  def test_1 
  end

  def test_2
  end
end

The fault in this is that you can't provide your own per-test setup and teardown methods unless you use the setup :method_name class method (only available in Rails 2.X?) and if you have a test suite or something that only runs one of the test methods, then the global_teardown won't be called because it assumes that all the test methods will be run eventually.

Daniel Beardsley
A: 

Use the TestSuite as @romulo-a-ceccon described for special preparations for each test suite.

However I think it should be mentioned here that Unit tests are ment to run in total isolation. Thus the execution flow is setup-test-teardown which should guarantee that each test run undisturbed by anything the other tests did.

Honza
+7  A: 

Rather than expend effort trying to get Test::Unit to suck less, stop wasting your time, and install Rspec instead.

It does exactly what you want, like this:

describe "Some Feature" do
  before :each do
    # this code runs once per-test
  end

  before :all do
    # this code runs once before all the tests
  end

  it "should do something useful, rather than just being called test1" do
    # el code here
  end
end
Orion Edwards
A: 

I created a mixin called SetupOnce. Here's an example of using it.

require 'test/unit'
require 'setuponce'


class MyTest < Test::Unit::TestCase
  include SetupOnce

  def self.setup_once
    puts "doing one-time setup"
  end

  def self.teardown_once
    puts "doing one-time teardown"
  end

end

And here is the actual code; notice it requires another module available from the first link in the footnotes.

require 'mixin_class_methods' # see footnote 1

module SetupOnce
  mixin_class_methods

  define_class_methods do
    def setup_once; end

    def teardown_once; end

    def suite
      mySuite = super

      def mySuite.run(*args)
        @name.to_class.setup_once
        super(*args)
        @name.to_class.teardown_once
      end

      return mySuite
    end
  end
end

# See footnote 2
class String
  def to_class
    split('::').inject(Kernel) {
      |scope, const_name|
      scope.const_get(const_name)
    }
  end
end

Footnotes:

  1. http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html

  2. http://infovore.org/archives/2006/08/02/getting-a-class-object-in-ruby-from-a-string-containing-that-classes-name/

+1  A: 

There are two more solutions to this problem. One of them (which I do not recommend), is to install the newest version of Test::Unit which has this support built in http://rubyforge.org/projects/test-unit/ to install just do gem install test-unit (or test-unit-full). I tried this and it worked great..... Until i tried to run my tests with rake. I could not run them because it doesn't appear the rake task that integrates rake with test::unit is not compatible. I tried a few obvious things but couldn't get that to work. The newest version supports lots of new features, checkout these features: http://www.slideshare.net/djberg96/new-features-of-test-unit-2x-presentation

class MyTest < Test::Unit::TestCase
    class << self
     def startup
      puts 'runs only once at start'
     end
     def shutdown
      puts 'runs only once at end'
     end
    end

    def setup
     puts 'runs before each test'
    end
    def teardown
     puts 'runs after each test'
    end 
    def test_stuff
     assert(true)
    end
end

Alright but that only works with the new version.. Well the other method is found in Hal Fulton's book "The Ruby Way". He overrides the self.suite method of Test::Unit which allows the test cases in a class to run as a suite.

def self.suite
    mysuite = super
    def mysuite.run(*args)
      MyTest.startup()
      super
      MyTest.shutdown()
    end
    mysuite
end

So, putting these to things together you can get this functionality with the current version of Test::Unit that comes with ruby (1.x) and be somewhat forward compatible using the following as an example:

class MyTest < Test::Unit::TestCase
    class << self
     def startup
      puts 'runs only once at start'
     end
     def shutdown
      puts 'runs only once at end'
     end
     def suite
      mysuite = super
      def mysuite.run(*args)
        MyTest.startup()
        super
        MyTest.shutdown()
      end
      mysuite
     end
    end

    def setup
     puts 'runs before each test'
    end
    def teardown
     puts 'runs after each test'
    end 
    def test_stuff
     assert(true)
    end
end
Matt Wolfe
Thanks for a great answer!
Adam Byrtek
A: 

Well, I accomplished basically the same way in a really ugly and horrible fashion, but it was quicker. :) Once I realized that the tests are run alphabetically:

class MyTests < Test::Unit::TestCase
def test_AASetup # I have a few tests that start with "A", but I doubt any will start with "Aardvark" or "Aargh!"
    #Run setup code
end

def MoreTests
end

def test_ZTeardown
    #Run teardown code
end

It aint pretty, but it works :)