单元测试

单元测试是一种后调试(post-debugging)测试技术,它允许你试运行程序的各个部分,以验证它们是否按预期工作。基本思想是你可以编写一些“断言”(assertions),说明某些行为应该获得某些结果。例如,你可能断言特定方法的返回值应为 100,或者它应该是布尔值(Boolean),或者它应该是特定类的实例。当测试运行时,如果断言被证明是正确的,即它通过了测试;如果不正确,则测试失败。

这是一个示例,如果对象 tgetVal 方法返回 100 以外的任何值,则会失败:

  1. assert_equal(100, t.getVal)

但是你不能只用这种断言来编写你的代码。测试有精确的规则。首先,你必须引入(require)test/unit 文件。然后,你需要从 TestCase 类派生一个测试类,该类位于 Unit 模块中,该模块本身则位于 Test 模块中:

  1. class MyTest < Test::Unit::TestCase

在这个类中,你可以编写一个或多个方法,每个方法构成一个包含一个或多个断言的测试。方法名称必须以 test 开头(因此名为 test1testMyProgram 的方法都可以,但是名为 myTestMethod 的方法不行)。这是一个测试,包含 TestClass.new(100).getVal 的返回值为 1000 的单个断言:

  1. def test2
  2. assert_equal(1000,TestClass.new(100).getVal)
  3. end

这里有一个完整的(虽然很简单)测试套件,我在其中定义了一个名为 MyTest 的 TestCase 类,它测试类 TestClass。在这里(有点想象力!),TestClass 可以用来代表我想要测试的整个程序:

test1.rb
  1. require 'test/unit'
  2. class TestClass
  3. def initialize( aVal )
  4. @val = aVal * 10
  5. end
  6. def getVal
  7. return @val
  8. end
  9. end
  10. class MyTest < Test::Unit::TestCase
  11. def test1
  12. t = TestClass.new(10)
  13. assert_equal(100, t.getVal)
  14. assert_equal(101, t.getVal)
  15. assert(100 != t.getVal)
  16. end
  17. def test2
  18. assert_equal(1000,TestClass.new(100).getVal)
  19. end
  20. end

此测试套件包含两个测试:test1(包含三个断言)和 test2(包含一个)。为了运行测试,你只需要运行该程序;你不必创建 MyClass 的实例。

你将看到结果报告,其中指出有两个测试,三个断言和一个失败。事实上,我做了四个断言。但是,在给定的测试中不会执行计算失败后的断言。在 test1 中,此断言失败:

  1. assert_equal(101, t.getVal)

失败后,下一个断言被跳过。如果我现在纠正这个(断言 100 而不是 101,那么下一个断言也将被测试:

  1. assert(100 != t.getVal)

这也失败了。这次报告指出已经执行计算了四个断言,其中一个失败。当然,在现实生活中,你应该设法写出正确的断言,当报告任何失败时,它应该是重写失败代码 - 而不是断言!

有关稍微复杂的测试示例,请参阅 test2.rb 程序(需要一个名为 buggy.rb 的文件)。这是一款小型冒险游戏,包括以下测试方法:

test2.rb
  1. def test1
  2. @game.treasures.each{ |t|
  3. assert(t.value < 2000, "FAIL: #{t} t.value = #{t.value}" )
  4. }
  5. end
  6. def test2
  7. assert_kind_of( TestMod::Adventure::Map, @game.map)
  8. assert_kind_of( Array, @game.map)
  9. end

这里第一个方法对传递给块的对象数组执行断言测试,当 value 属性不小于 2000 时,它会失败。第二个方法使用 assert_kind_of 方法测试两个对象的类类型。当发现 @game.map 属于 TestMod::Adventure::Map 而不是被断言的 Array 时,此方法中的第二个测试会失败。

该代码还包含另外两个名为 setupteardown 的方法。定义时,将在每个测试方法之前和之后运行具有这些名称的方法。换句话说,在 test2.rb 中,以下方法将按以下顺序运行:setuptest1teardownsetuptest2teardown。这使你有机会在运行每个测试之前将任何变量重新初始化为特定值,或者在这种情况下,重新创建对象以确保它们处于已知状态:

  1. def setup
  2. @game = TestMod::Adventure.new
  3. end
  4. def teardown
  5. @game.endgame
  6. end