跳转到内容

Ruby 编程/单元测试

来自维基教科书,开放世界中的开放书籍

← 异常 | RubyDoc →


单元测试 是一种在开发过程早期发现错误的好方法,前提是您投入时间编写适当且有用的测试。与其他语言一样,Ruby 在其标准库中提供了一个框架,用于设置、组织和运行名为 Test::Unit 的测试。

还有其他非常流行的测试框架,rspec 和 cucumber 就是其中之一。

具体来说,Test::Unit 提供了三种基本功能

  1. 一种定义基本通过/失败测试的方法。
  2. 一种将相关测试收集在一起并作为一组运行的方法。
  3. 运行单个测试或整组测试的工具。

简单介绍

[编辑 | 编辑源代码]

首先创建一个新类。

# 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 提供了setupteardown

# 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

成员函数,它们会在每个测试(成员函数)之前和之后运行。

练习

[编辑 | 编辑源代码]

自行车轮胎半径

[编辑 | 编辑源代码]

实现一个具有公共方法的类,该方法可以解决以下问题:用户有一个任意周长的橡胶自行车轮胎。当一侧被切开,自行车轮胎被拉伸成一条直线时,它的长度被测量为任意长度。根据该长度,确定自行车轮胎最初的半径。

外部链接
华夏公益教科书