单元测试
单元测试是一种后调试(post-debugging)测试技术,它允许你试运行程序的各个部分,以验证它们是否按预期工作。基本思想是你可以编写一些“断言”(assertions),说明某些行为应该获得某些结果。例如,你可能断言特定方法的返回值应为 100,或者它应该是布尔值(Boolean),或者它应该是特定类的实例。当测试运行时,如果断言被证明是正确的,即它通过了测试;如果不正确,则测试失败。
这是一个示例,如果对象 t
的 getVal
方法返回 100 以外的任何值,则会失败:
assert_equal(100, t.getVal)
但是你不能只用这种断言来编写你的代码。测试有精确的规则。首先,你必须引入(require)test/unit 文件。然后,你需要从 TestCase 类派生一个测试类,该类位于 Unit 模块中,该模块本身则位于 Test 模块中:
class MyTest < Test::Unit::TestCase
在这个类中,你可以编写一个或多个方法,每个方法构成一个包含一个或多个断言的测试。方法名称必须以 test 开头(因此名为 test1
或 testMyProgram
的方法都可以,但是名为 myTestMethod
的方法不行)。这是一个测试,包含 TestClass.new(100).getVal
的返回值为 1000 的单个断言:
def test2
assert_equal(1000,TestClass.new(100).getVal)
end
这里有一个完整的(虽然很简单)测试套件,我在其中定义了一个名为 MyTest 的 TestCase 类,它测试类 TestClass。在这里(有点想象力!),TestClass 可以用来代表我想要测试的整个程序:
require 'test/unit'
class TestClass
def initialize( aVal )
@val = aVal * 10
end
def getVal
return @val
end
end
class MyTest < Test::Unit::TestCase
def test1
t = TestClass.new(10)
assert_equal(100, t.getVal)
assert_equal(101, t.getVal)
assert(100 != t.getVal)
end
def test2
assert_equal(1000,TestClass.new(100).getVal)
end
end
此测试套件包含两个测试:test1
(包含三个断言)和 test2
(包含一个)。为了运行测试,你只需要运行该程序;你不必创建 MyClass 的实例。
你将看到结果报告,其中指出有两个测试,三个断言和一个失败。事实上,我做了四个断言。但是,在给定的测试中不会执行计算失败后的断言。在 test1
中,此断言失败:
assert_equal(101, t.getVal)
失败后,下一个断言被跳过。如果我现在纠正这个(断言 100 而不是 101,那么下一个断言也将被测试:
assert(100 != t.getVal)
这也失败了。这次报告指出已经执行计算了四个断言,其中一个失败。当然,在现实生活中,你应该设法写出正确的断言,当报告任何失败时,它应该是重写失败代码 - 而不是断言!
有关稍微复杂的测试示例,请参阅 test2.rb 程序(需要一个名为 buggy.rb 的文件)。这是一款小型冒险游戏,包括以下测试方法:
def test1
@game.treasures.each{ |t|
assert(t.value < 2000, "FAIL: #{t} t.value = #{t.value}" )
}
end
def test2
assert_kind_of( TestMod::Adventure::Map, @game.map)
assert_kind_of( Array, @game.map)
end
这里第一个方法对传递给块的对象数组执行断言测试,当 value 属性不小于 2000 时,它会失败。第二个方法使用 assert_kind_of
方法测试两个对象的类类型。当发现 @game.map
属于 TestMod::Adventure::Map
而不是被断言的 Array
时,此方法中的第二个测试会失败。
该代码还包含另外两个名为 setup
和 teardown
的方法。定义时,将在每个测试方法之前和之后运行具有这些名称的方法。换句话说,在 test2.rb 中,以下方法将按以下顺序运行:setup
,test1
,teardown
,setup
,test2
,teardown
。这使你有机会在运行每个测试之前将任何变量重新初始化为特定值,或者在这种情况下,重新创建对象以确保它们处于已知状态:
def setup
@game = TestMod::Adventure.new
end
def teardown
@game.endgame
end