Ruby 编程/单元测试
单元测试 是一种在开发过程早期发现错误的好方法,前提是您投入时间编写适当且有用的测试。与其他语言一样,Ruby 在其标准库中提供了一个框架,用于设置、组织和运行名为 Test::Unit 的测试。
还有其他非常流行的测试框架,rspec 和 cucumber 就是其中之一。
具体来说,Test::Unit 提供了三种基本功能
- 一种定义基本通过/失败测试的方法。
- 一种将相关测试收集在一起并作为一组运行的方法。
- 运行单个测试或整组测试的工具。
首先创建一个新类。
# File: simple_number.rb
class SimpleNumber
def initialize(num)
raise unless num.is_a?(Numeric)
@x = num
end
def add(y)
@x + y
end
def multiply(y)
@x * y
end
end
让我们从一个例子开始,测试 SimpleNumber 类。
# File: tc_simple_number.rb
require_relative "simple_number"
require "test/unit"
class TestSimpleNumber < Test::Unit::TestCase
def test_simple
assert_equal(4, SimpleNumber.new(2).add(2) )
assert_equal(6, SimpleNumber.new(2).multiply(3) )
end
end
它产生
>> ruby tc_simple_number.rb
Loaded suite tc_simple_number
Started
.
Finished in 0.002695 seconds.
1 tests, 2 assertions, 0 failures, 0 errors
这里发生了什么?我们定义了一个类 TestSimpleNumber,它继承自 Test::Unit::TestCase。在 TestSimpleNumber 中,我们定义了一个名为 test_simple 的成员函数。该成员函数包含一些简单的 断言,这些断言会执行我的类。当我们运行该类时(请注意,我没有在它周围放置任何包装代码——它只是一个类定义),测试会自动运行,我们会被告知我们已经运行了 1 个测试和 2 个断言。
让我们尝试一个更复杂的例子。
# File: tc_simple_number2.rb
require_relative "simple_number"
require "test/unit"
class TestSimpleNumber < Test::Unit::TestCase
def test_simple
assert_equal(4, SimpleNumber.new(2).add(2) )
assert_equal(4, SimpleNumber.new(2).multiply(2) )
end
def test_typecheck
assert_raise( RuntimeError ) { SimpleNumber.new('a') }
end
def test_failure
assert_equal(3, SimpleNumber.new(2).add(2), "Adding doesn't work" )
end
end
>> ruby tc_simple_number2.rb
Loaded suite tc_simple_number2
Started
F..
Finished in 0.038617 seconds.
1) Failure:
test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:
Adding doesn't work.
<3> expected but was
<4>.
3 tests, 4 assertions, 1 failures, 0 errors
现在类中有三个测试(三个成员函数)。函数 test_typecheck 使用 assert_raise 检查异常。函数 test_failure 被设置为失败,Ruby 输出愉快地指出这一点,不仅告诉我们哪个测试失败,还告诉我们它如何失败(预期 <3> 但实际为 <4>)。在这个断言中,我们还添加了一个最终参数,这是一个自定义错误消息。它是严格可选的,但对于调试可能很有用。所有断言都包含自己的错误消息,这些消息通常足以进行简单的调试。
Test::Unit 提供了一套丰富的断言,这些断言在 Ruby-Doc 中有详细的文档记录。以下是一些简短的摘要(断言及其否定被分组在一起。文本描述通常针对列出的第一个断言——名称应该有一定的逻辑意义)
assert( boolean, [message] ) | 如果 boolean 为真,则返回真。 |
assert_equal( expected, actual, [message] ) assert_not_equal( expected, actual, [message] ) |
如果 expected == actual,则返回真。 |
assert_match( pattern, string, [message] ) assert_no_match( pattern, string, [message] ) |
如果 string =~ pattern,则返回真。 |
assert_nil( object, [message] ) assert_not_nil( object, [message] ) |
如果 object == nil,则返回真。 |
assert_in_delta( expected_float, actual_float, delta, [message] ) | 如果 (actual_float - expected_float).abs <= delta,则返回真。 |
assert_instance_of( class, object, [message] ) | 如果 object.class == class,则返回真。 |
assert_kind_of( class, object, [message] ) | 如果 object.kind_of?(class),则返回真。 |
assert_same( expected, actual, [message]) assert_not_same( expected, actual, [message] ) |
如果 actual.equal?( expected ),则返回真。 |
assert_raise( Exception,... ) {block} assert_nothing_raised( Exception,...) {block} |
如果代码块引发(或不引发)列出的异常之一,则返回真。 |
assert_throws( expected_symbol, [message] ) {block} assert_nothing_thrown( [message] ) {block} |
如果代码块抛出(或不抛出)expected_symbol,则返回真。 |
assert_respond_to( object, method, [message] ) | 如果对象可以响应给定方法,则返回真。 |
assert_send( send_array, [message] ) | 如果使用给定参数发送到对象的方法返回真,则返回真。 |
assert_operator( object1, operator, object2, [message] ) | 使用给定运算符比较两个对象,如果 true,则通过。 |
特定代码单元的测试被分组到一个 测试用例 中,它是一个 Test::Unit::TestCase 的子类。断言被收集在 测试 中,测试用例的成员函数,其名称以test_开头。当执行或需要测试用例时,Test::Unit 将迭代测试用例中的所有测试(使用反射查找所有以 test_ 开头的成员函数),并提供适当的反馈。
测试用例类可以被收集到 测试套件 中,测试套件是需要其他测试用例的 Ruby 文件
# File: ts_all_the_tests.rb
require 'test/unit'
require 'test_one'
require 'test_two'
require 'test_three'
这样,相关的测试用例可以自然地分组。此外,测试套件可以包含其他测试套件,从而允许构建测试层次结构。
这种结构提供了对测试的相对细粒度控制。可以从测试用例运行单个测试(见下文),可以单独运行完整的测试用例,可以运行包含多个用例的测试套件,也可以运行多个套件,跨越多个测试用例。
Test::Unit 的作者 Nathaniel Talbott 建议使用tc_作为测试用例名称的开头,使用ts_
作为测试套件名称的开头。
运行特定测试>> ruby -w tc_simple_number2.rb --name test_typecheck
Loaded suite tc_simpleNumber2
Started
.
Finished in 0.003401 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
可以从完整的测试用例中运行一个(或多个)测试
>> ruby -w tc_simple_number2.rb --name /test_type.*/
Loaded suite tc_simpleNumber2
Started
.
Finished in 0.003401 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
也可以运行名称与给定模式匹配的所有测试
设置和拆卸[编辑 | 编辑源代码]在许多情况下,需要在每个测试之前和/或之后运行一小段代码。Test::Unit 提供了setup和teardown
# File: tc_simple_number3.rb
require "./simple_number"
require "test/unit"
class TestSimpleNumber < Test::Unit::TestCase
def setup
@num = SimpleNumber.new(2)
end
def teardown
## Nothing really
end
def test_simple
assert_equal(4, @num.add(2) )
end
def test_simple2
assert_equal(4, @num.multiply(2) )
end
end
>> ruby tc_simple_number3.rb
Loaded suite tc_simple_number3
Started
..
Finished in 0.00517 seconds.
2 tests, 2 assertions, 0 failures, 0 errors
成员函数,它们会在每个测试(成员函数)之前和之后运行。
练习实现一个具有公共方法的类,该方法可以解决以下问题:用户有一个任意周长的橡胶自行车轮胎。当一侧被切开,自行车轮胎被拉伸成一条直线时,它的长度被测量为任意长度。根据该长度,确定自行车轮胎最初的半径。