In Ruby's Test::Unit::TestCase, how do I override the initialize method?
Asked Answered
R

10

29

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?

Recto answered 1/11, 2008 at 19:8 Comment(2)
Two test methods with the same name leads to the first method not being run. You could put a flunk in the first test, and tests would still pass. One side-effect of cut and paste programming.Lozenge
Yes, and it is easy. This is finally implemented in TestUnit. See my post waaaay down this page.Platus
P
28

As mentioned 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

Here is 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
Penicillium answered 22/4, 2009 at 19:9 Comment(5)
Unfortunately the second answer doesn't work with the current version of Test::Unit, at least not running in RubyMine under Windows 7. Cutting and pasting the code into RubyMine and running it, startup and shutdown each run twice, not once :( I'm only using it to log the start and end of a test case (using Logger), so I can see which test case produced which bit of the log, so I can reluctantly live with that, but other people might not be able to.Polychromy
What if I want to do a similar thing in ActionController::TestCase ?Forget
This answer is like 6 years old so I wouldn't be surprised if things have changed slightly since given that it relies on some pretty specific implementation details of TestUnit::TestCase.Penicillium
@MattWolfe the methods startup and shutdown have been implemented in Ruby since this answer. They are called at the same time they are in your answer. So this answer is correct if you remove the calls to MyTest.startup() and MyTest.shutdown(), as these are now automatically called by super.Aldous
glad they added this (wrote the answer 6 years ago). I doubt much here still applies. I think I'll leave it for historical purposes.Penicillium
P
11

FINALLY, test-unit has this implemented! Woot! If you are using v 2.5.2 or later, you can just use this:

Test::Unit.at_start do
  # initialization stuff here
end

This will run once when you start your tests off. There are also callbacks which run at the beginning of each test case (startup), in addition to the ones that run before every test (setup).

http://test-unit.rubyforge.org/test-unit/en/Test/Unit.html#at_start-class_method

Platus answered 25/9, 2012 at 14:53 Comment(1)
This is the answer for me. Works exactly as you'd expect, and doesn't bloat my tests with pointless boilerplate. Great stuff!Child
D
10

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.

Dorothy answered 1/11, 2008 at 20:25 Comment(1)
why am I getting an error " uninitialized constant Test::Unit::TestSuite"?Rosner
H
2

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 :)

Hutson answered 14/7, 2009 at 16:31 Comment(0)
D
2

To solve this problem I used the setup construct, with only one test method followed. This one testmethod is calling all other tests.

For instance

class TC_001 << Test::Unit::TestCase
  def setup
    # do stuff once
  end

  def testSuite
    falseArguments()
    arguments()
  end

  def falseArguments
    # do stuff
  end

  def arguments
    # do stuff
  end
end
Diffract answered 7/11, 2010 at 23:2 Comment(0)
I
2

I know this is quite an old post, but I had the issue (and had already written classes using Tes/unit) and ave answered using another method, so if it can help...

If you only need the equivalent of the startup function, you can use the class variables:

class MyTest < Test::Unit::TestCase
  @@cmptr = nil
  def setup
    if @@cmptr.nil?
      @@cmptr = 0
      puts "runs at first test only"
      @@var_shared_between_fcs = "value"
    end
    puts 'runs before each test'
  end
  def test_stuff
    assert(true)
  end
end
India answered 17/9, 2013 at 12:53 Comment(1)
I've read all the answers here. This is the only one that makes 100% clear sense to me - the others just are so obscure and complex and makes my teensy caffeeine-lacking brain wanna vomit.Dichlorodifluoromethane
H
1

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.

Harker answered 1/11, 2008 at 21:2 Comment(0)
D
0

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.

Diazole answered 1/11, 2008 at 21:48 Comment(0)
A
0

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/

Antherozoid answered 15/12, 2008 at 21:7 Comment(0)
M
0

+1 for the RSpec answer above by @orion-edwards. I would have commented on his answer, but I don't have enough reputation yet to comment on answers.

I use test/unit and RSpec a lot and I have to say ... the code that everyone has been posting is missing a very important feature of before(:all) which is: @instance variable support.

In RSpec, you can do:

describe 'Whatever' do
  before :all do
    @foo = 'foo'
  end

  # This will pass
  it 'first' do
    assert_equal 'foo', @foo
    @foo = 'different'
    assert_equal 'different', @foo
  end

  # This will pass, even though the previous test changed the 
  # value of @foo.  This is because RSpec stores the values of 
  # all instance variables created by before(:all) and copies 
  # them into your test's scope before each test runs.
  it 'second' do
    assert_equal 'foo', @foo
    @foo = 'different'
    assert_equal 'different', @foo
  end
end

The implementations of #startup and #shutdown above all focus on making sure that these methods only get called once for the entire TestCase class, but any instance variables used in these methods would be lost!

RSpec runs its before(:all) in its own instance of Object and all of the local variables are copied before each test is run.

To access any variables that are created during a global #startup method, you would need to either:

  • copy all of the instance variables created by #startup, like RSpec does
  • define your variables in #startup into a scope that you can access from your test methods, eg. @@class_variables or create class-level attr_accessors that provide access to the @instance_variables that you create inside of def self.startup

Just my $0.02!

Mathewmathews answered 13/8, 2011 at 17:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.