28

我正在努力使用 Test::Unit。当我想到单元测试时,我会想到每个文件一个简单的测试。但在 Ruby 的框架中,我必须改为:

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

   def test_1 
   end

   def test_1 
   end
end

但是每次调用 test_* 方法都会运行 setup 和 teardown 。这正是我不想要的。相反,我想要一个只为整个班级运行一次的设置方法。但是我似乎无法在不破坏 TestCase 的初始化的情况下编写自己的 initialize()。

那可能吗?还是我让这一切变得复杂到无可救药?

4

10 回答 10

28

正如 Hal Fulton 的书“The Ruby Way”中提到的那样。他重写了 Test::Unit 的 self.suite 方法,该方法允许类中的测试用例作为套件运行。

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

这是一个例子:

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
于 2009-04-22T19:09:39.047 回答
11

最后,测试单元已经实现了!哇! 如果您使用的是 v 2.5.2 或更高版本,则可以使用:

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

这将在您开始测试时运行一次。除了在每个测试(设置)之前运行的回调之外,还有在每个测试用例(启动)开始时运行的回调。

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

于 2012-09-25T14:53:54.697 回答
10

这就是它应该如何工作!

每个测试都应该与其他测试完全隔离,因此每个测试用例都执行一次setupandtear_down方法。但是,在某些情况下,您可能希望对执行流程进行更多控制。然后,您可以将测试用例分组到suites中。

在您的情况下,您可以编写如下内容:

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

TestDecorator定义了一个特殊的套件,它提供了一个setupand方法,该tear_down方法在运行它包含的一组测试用例之前和之后只运行一次。

这样做的缺点是您需要告诉Test::Unit如何在单元中运行测试。如果您的单元包含许多测试用例并且您只需要其中一个的装饰器,您将需要这样的东西:

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)

Test::Unit文档文档很好地解释了套件的工作原理。

于 2008-11-01T20:25:37.927 回答
2

好吧,我以一种非常丑陋和可怕的方式完成了基本相同的方式,但它更快。:) 一旦我意识到测试是按字母顺序运行的:

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

它不漂亮,但它有效:)

于 2009-07-14T16:31:38.770 回答
2

我知道这是一篇相当老的帖子,但我遇到了这个问题(并且已经使用 Tes/unit 编写了类)并且已经使用另一种方法进行了回答,所以如果它可以帮助......

如果只需要等价的启动函数,可以使用类变量:

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
于 2013-09-17T12:53:25.200 回答
2

为了解决这个问题,我使用了 setup 构造,只使用了一种测试方法。这一种测试方法正在调用所有其他测试。

例如

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
于 2010-11-07T23:02:03.287 回答
1

我遇到了这个确切的问题并创建了一个子类Test::Unit::TestCase来完全按照您的描述进行操作。

这就是我想出的。它提供了它自己的setup方法teardown来计算类中以“test”开头的方法的数量。在第一次调用setup它时调用global_setup,最后一次调用teardown它调用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

像这样创建您的测试用例:

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

这样做的错误是,除非您使用类方法(仅在 Rails 2.X 中可用?)并且如果您有一个测试套件或仅运行一个测试的东西,否则您无法提供自己的每个测试setup和方法方法,则不会调用,因为它假定所有测试方法最终都会运行。teardownsetup :method_nameglobal_teardown

于 2008-11-01T21:02:31.190 回答
0

将 TestSuite 用作 @romulo-a-ceccon 描述的每个测试套件的特殊准备。

但是我认为这里应该提到单元测试是完全隔离运行的。因此执行流程是 setup-test-teardown 这应该保证每个测试运行不受其他测试所做的任何干扰。

于 2008-11-01T21:48:21.847 回答
0

我创建了一个名为 SetupOnce 的 mixin。这是一个使用它的例子。

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

这是实际的代码;请注意,它需要脚注中第一个链接中提供的另一个模块。

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

脚注:

  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-c​​lasses-name/

于 2008-12-15T21:07:16.730 回答
0

+1 为@orion-edwards 提供的上述 RSpec 答案。我会评论他的答案,但我还没有足够的声誉来评论答案。

我经常使用 test/unitRSpec,我不得不说......每个人发布的代码都缺少一个非常重要的特性before(:all):@instance 变量支持。

在 RSpec 中,您可以执行以下操作:

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

最重要的是确保这些方法的实现#startup#shutdown为整个TestCase类调用一次,但是这些方法中使用的任何实例变量都将丢失!

RSpecbefore(:all)在它自己的 Object 实例中运行它,并且在每个测试运行之前复制所有局部变量。

要访问在全局方法期间创建的任何变量#startup,您需要:

  • 复制由创建的所有实例变量#startup,就像 RSpec 一样
  • 将变量定义#startup到可以从测试方法访问的范围内,例如。@@class_variables或创建类级 attr_accessors 提供对@instance_variables您在其中创建的访问权限def self.startup

只是我的 0.02 美元!

于 2011-08-13T17:10:19.750 回答