使用 DB2 和 Ruby on Rails 进行测试

灵活的应用程序开发的一项关键需求是不断地进行代码集成,确保回归测试成功。Ruby on Rails 框架可以大大简化这个任务。与其他著名的快速应用程序开发环境一样,Ruby 支持单元测试和功能测试;但是它还能够通过智能化的模板自动生成大多数基础设施、目录、测试文件和测试用例。在 DB2 与 Ruby on Rails 系列 的第三篇文章中,我们将对第二篇文章中的 Team Room 演示程序应用 Rails 内置的测试框架。

简介

在 “DB2 与 Ruby on Rails” 系列的 第 1 部分 中,我们介绍了 IBM_DB Ruby 驱动程序、Rails 迁移和 Team Room 应用程序。在 第 2 部分 中,改进了现有的 Team Room 应用程序,学习了如何在 Rails 应用程序中利用 DB2® pureXML™ 支持。

既然 Team Room 应用程序已经扩展了,变得更加复杂了,就要确保这个应用程序的运行符合预期。很自然,下一步就是测试 Team Room 应用程序。本文讲解如何在 DB2 on Rails 环境中进行测试,以及如何在 Ruby on Rails 框架中编写测试。

Rails 框架为测试提供了内置的支持,可以方便地测试 web 应用程序。在 Rails 中,可以在编写和运行核心应用程序之后编写测试,也可以在编写应用程序的同时编写测试,甚至 可以在编写应用程序之前编写测试【这称为测试驱动的开发(TDD)】。

测试 Team Room 应用程序

在创建新的 Rails 项目时,Rails 会替您生成测试基础设施。如果进入 Rails Team Room 项目目录 D:\rails\teamroom\,就会看到 \test 子目录。在 \test 下面,会看到 5 个目录和一个 helper 文件:


清单 1. Test 目录的内容
                
/unit
/functional
/fixtures
/integration
/mocks
test_helper.rb

将您的所有测试放在 /test 目录中,特殊的测试根据性质和功能分别放在相应的子目录中。下面解释一下 /test 目录中的每个组件:

Unit

在 Rails 中,为测试模型编写的测试称为单元测试(unit test)。一般情况下,要为每个模型编写一个单元测试。在单元测试中,要测试所有可能破坏模型逻辑的东西。基本测试应该包括对检验代码和断言以及数据库操作(比如创建、读取、更新和删除,CRUD)的测试。

Functional

为测试控制器编写的测试称为功能测试(functional test)。它们在高于单元测试的层次上测试应用程序。同样,一般情况下要为每个控制器编写一个功能测试。功能测试的示例包括测试成功的 Web 请求、正确的页面重定向、正确的身份验证以及对特定动作的正确响应。

Fixtures

fixture 指定一个模型中的内容。在 fixture 中指定数据,并让 Rails 在运行单元测试时装载这些数据。这有点儿像 DB2 中的导入或装载函数。

Mocks

mock 是一个 “伪” 对象,它让我们能够集中精力测试 Rails 项目范围内的重要特性,而不必关注与无法控制的外部系统的交互。例如,当新文档添加到用户订阅的特定主题时,Team Room 应用程序会通过电子邮件通知他们。我们并不使用电子邮件检验系统来检验用户提供的地址是否是有效的电子邮件地址。相反,我们依靠电子邮件帐户提供商或域服务器提供的错误报告来检验用户提供的电子邮件地址。为了测试 Team Room 的通知功能,而不必访问外部网络,也不使用电子邮件检验系统,我们可以编写 mock 测试方法。

Integration

集成测试是涉及多个控制器和动作的测试。顾名思义,这种测试确保不同的控制器和动作按照预期的方式配合工作。集成测试一般比较完整,因为它们覆盖 Rails 应用程序中各个组件之间的高层交互。

test_helper.rb

test_helper.rb 文件建立多个测试用例共有的许多默认 Rails 测试行为。例如,在 test_helper.rb 中,RAILS_ENV 环境变量设置为 “test”,所以 Rails 会使用测试数据库进行测试(在 database.yml 文件中配置)。所有单元测试都装载 test_helper.rb 文件。在 test_helper.rb 中还可以定义定制的断言,这样同一个 Rails 应用程序中的多个测试就可以共享这些定制的断言。

设置各个数据库环境

在 “DB2 与 Ruby on Rails” 系列的 第 1 部分 中,我们编辑了 D:\rails\teamroom\config\database.yml 文件,从而建立到 DB2 XMLDB 开发数据库的连接。在这个 YAML 文件的 “development” 部分下面,有另外两个部分 “test” 和 “production”,我们还没有填写这些部分。显然,要在 “test” 部分中指定测试数据库环境,在 “production” 部分中指定生产数据库环境。在现实中,开发环境和测试环境可能指向同一个数据库,但是生产环境肯定应该与开发或测试数据库分开。现在,我们来创建 TESTDB 测试数据库,并在 database.yml 文件中设置测试环境。

创建 TESTDB 数据库:

db2 create db testdb using codeset utf-8 territory us

按照以下清单编辑 database.yml:


清单 2. database.yml 文件
                
# IBM DB2 Database configuration file
#
# Install the IBM DB2 driver and get assistance from:
# http://www.alphaworks.ibm.com/tech/db2onrails
development:
  adapter:      ibm_db
  database:     xmldb
  username:     user
  password:     secret
  schema:       teamroom
  application:  TeamRoom
  account:      devuser
  workstation:  devbox
# == remote TCP/IP connection (required when no local database catalog entry available)
# host:         bigserver     // fully qualified hostname or IP address
# port:         50000         // data server TCP/IP port number

test:
  adapter:      ibm_db
  database:     testdb
  username:     testuser
  password:     secret
  schema:       teamroom
  application:  TeamRoom
  account:      testuser
  workstation:  testbox

production:
  adapter:      ibm_db
  database:     proddb
  username:     produser
  password:     secret
  schema:       teamroom
  application:  TeamRoom
  account:      produser
  workstation:  prodbox

为了进行测试,我们希望确保测试数据库具有与开发数据库相同的表结构。我们不必管理那些装载数据库模式的 DDL 脚本,而是使用 Rake 命令帮助创建测试环境。运行 rake --tasks 命令,就会显示几个用来构建或清空测试数据库的命令:


清单 3. Rake --tasks 输出
                
rake db:test:clone             # Recreate the test database from the current 
                               # environment's  database schema
rake db:test:clone_structure   # Recreate the test databases from the development 
                               # structure
rake db:test:prepare           # Prepare the test database and load the schema
rake db:test:purge             # Empty the test database

我们用 rake db:test:prepare 命令在数据库中建立表。这个命令会将开发数据库中的数据库模式复制到测试数据库中:


清单 4. rake db:test:prepare
                
D:\rails\teamroom> rake db:test:prepare
(in D:\rails\teamroom)

现在可以连接 testdb 数据库,检查所有表是否已经成功地创建了:


清单 5. testdb 中的表
                
D:\rails\teamroom>db2 list tables for schema teamroom

Table/View                      Schema          Type  Creation time
------------------------------- --------------- ----- --------------------------
DOCUMENTS                       TEAMROOM        T     2007-06-05-11.21.46.343002
SCHEMA_INFO                     TEAMROOM        T     2007-06-05-11.21.48.306001
SUBJECTS                        TEAMROOM        T     2007-06-05-11.21.46.633003
SUBJECTS_SUBSCRIPTIONS          TEAMROOM        T     2007-06-05-11.21.47.004000
SUBSCRIPTIONS                   TEAMROOM        T     2007-06-05-11.21.47.194005
USERS                           TEAMROOM        T     2007-06-05-11.21.47.595003
XML_CONTENTS                    TEAMROOM        T     2007-06-05-11.21.47.955001

  
  7 record(s) selected.

在测试数据库中定义了所有表之后,现在可以装载测试数据。尽管可以通过 SQL 插入或导入手工装载测试数据,但是更简单(也更容易管理)的方法是使用文本 fixture,稍后讨论这个问题。

Ruby 内置的单元测试

Ruby(1.8.1 和更高版本)附带一个关键的库 test/unit。为了编写 Ruby 测试脚本,我们:

  1. 在 .rb 文件的顶部指定 “require 'test/unit'”。这个语句将装载 Test::Unit 模块。
  2. 在 require 语句后面,添加一个类声明,指出这个类是 Test::Unit::TestCase 的子类(见清单 6)。

清单 6. 为单元测试指定 “require 'test/unit'”
                
require 'test/unit'

class SubTestCase < Test::Unit::TestCase
  ...
end

在 Team Room 应用程序路径的 test\unit 子目录中,可以找到与所有(通过框架创建的)模型对象对应的 .rb 文件。这些文件名是在对象名后面加上 “_test”。

在 test\unit 子目录下创建测试脚本时,不需要指定 require 'test/unit' 语句。现有的 require 语句引用了 test_helper.rb,而 test_helper.rb 已经包含必要的信息。


清单 7. test\unit 路径列表
                
D:\rails\teamroom\test\unit>dir
 Volume in drive D has no label.
 Volume Serial Number is C8D3-5819

 Directory of D:\rails\teamroom\test\unit

06/05/2007  09:27 AM    

带 “test_” 前缀的方法

Ruby 中的所有测试方法名必须以 “test” 开头,否则测试框架就不知道它是一个测试。如果没有 “test” 前缀,它就被当作一般的 Ruby 方法,测试框架不会自动运行它。


清单 8. 一个简单的单元测试
                
require File.dirname(__FILE__) + '/../test_helper'

class Sub_Test < Test::Unit::TestCase
  def test_truth
    assert true
  end
end

现在可以将这个文件保存为 sub_test.rb。在执行时,它产生以下输出。


清单 9. 运行 sub_test.rb
                
D:\rails\teamroom\test\unit>ruby sub_test.rb
Loaded suite sub_test
Started
.
Finished in 0.631 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

上面这个简单的测试场景执行一个断言,由于使用了 “assert true” 语句,这个测试总会成功。断言就是测试,正如在下一节中看到的,有许多可以用来测试 Rails 应用程序的断言语句。

在 Rails 中,对于每个用 script/generate model、script/generate controller 和 script/generate scaffold 创建的模型和控制器,都在 test 子目录中创建一个对应的测试存根。在使用这些测试存根时,不需要指定 “require 'test/unit'”。

断言

断言检查应用程序或模块的前提条件和后续条件,确保它们处于有效的状态。Test::Unit 断言采用一种基本模式:第一个参数是预期的结果,第二个参数是实际结果。当预期值和实际值不相符时,生成一个消息来指出错误。

Test::Unit::Assertions 中可以找到核心 Ruby 断言的列表。

还有许多可用的断言。Module Test::Unit::Assertions Information 提供了相关信息。

正如前面提到的,断言用来确保有效状态。那么,有效状态如何定义?

在大多数情况下,有效状态由代码中的检验控制。

使用检验

在将输入数据存储到数据库之前,可以对它进行检验。为此,可以定义定制的检验或使用标准的检验集。对于标准的检验(比如检查属性的名称、长度和格式),可以使用 Active Record 提供的检验。

如果在模型中添加检验方法,那么在对象保存到数据库中之前就会执行检验。如果检验通过了,那么对象被写到数据库。如果检验失败,那么把与失败的对象检验方法相关联的错误消息添加到错误列表。可以显示这个列表,帮助用户采取适当的措施来纠正失败。

本文将用一些示例演示如何用 Active Record 的检验器集检验模型。

对于 Team Room 示例,假设我们希望对主题做以下检验:

  • 与这个主题相关联的所有文档对象都是有效对象
  • 主题名非空
  • 主题名的长度不超过 20 个字符

为了进行这些检验,可以使用一些标准的检验方法。我们在 app\models\subject.rb 中添加以下检验:


清单 10. app\models\subject.rb 中的检验
                
  validates_associated      :documents
  validates_associated      :subscriptions				
  validates_presence_of     :name
  validates_length_of       :name,
                            :maximum => 20,   # maximum 20 characters
                            :too_long => "is too long, maximum 20 characters"
  validates_numericality_of :size,            # value is numeric and only integer accepted
                            :only_integer => true,
                            :greater_than => 0                             
  validates_presence_of     :description
  validates_length_of       :tag,
                            :maximum => 10,   # maximum 10 characters
                            :too_long => "is too long, maximum 10 characters"   

“validates_associated” 方法检查与这个主题相关联的所有文档是否有效。“validates_presence_of” 方法检查主题名是否非空。

使用 “validates_length_of” 方法检查主题名的长度是否不超过 20 个字符。使用 “maximum” 配置选项指定主题名的最大大小。“too_long” 选项指定主题名的长度超过 20 个字符时生成的错误消息,它用来替换 “message” 选项。在默认情况下,“too_long” 消息是 “is too long (maximum is %d characters)”。

现在根据上面设定的规则检验主题输入数据。如果任何用户输入了有问题的数据,我们希望显示错误消息列表,让用户可以纠正输入数据。

下面是可以使用的 ActionView 方法:

  • error_message_on —— 返回指定的对象属性的错误字符串
  • error_messages_for —— 返回指定对象的错误列表

这里 可以找到关于这些方法的更多信息。

在 Team Room 示例中,如果希望列出为新主题输入的所有输入数据错误消息,可以在 app\views\subjects\_form.rhtml 的顶部添加以下代码。


清单 11. 输入数据错误消息
                


这个方法调用将返回一个包含所有错误消息的 div(如果有错误消息的话),可以很容易地显示它:


图 1. 当输入的主题名太长时显示的错误消息
错误消息

这里 可以找到可以在类范围中定义的 Active Record 模型检验列表。

既然已经建立了主题模型检验,就可以使用断言实际测试上面的检验了。

使用断言

现在可以使用断言测试与 SUBJECTS 表相关联的模型。与 SUBJECTS 表相关联的模型之一确保用必需的有效信息填充这个表。对于避免在后端数据库上执行不必要的装载,这些检验非常重要。测试的检验见 \app\models\subject.rb(见 清单 10)。

“test_invalid_with_empty_values” 是一个简单的示例,它演示如何检查空值的错误条件。在 “test_invalid_with_empty_values” 方法中,测试空主题值是否导致错误。


清单 12. 测试方法 “test_invalid_with_empty_values”
                
require File.dirname(__FILE__) + '/../test_helper'

class SubjectTest < Test::Unit::TestCase

# Tests validation failure using empty values
  def test_invalid_with_empty_values
    subject = Subject.new
    assert !subject.valid?
    assert subject.errors.invalid?(:name)
    assert subject.errors.invalid?(:size)
    assert subject.errors.invalid?(:description)
  end
  ...

现在,再添加两个测试,一个测试检查有效的主题数据,另一个测试检查在指定不正确的 TAG 长度时是否显示期望的错误消息。

下面三个测试方法应该放在 test/unit/subject_test.rb 中:


清单 13. subject_test.rb 的测试方法
                
require File.dirname(__FILE__) + '/../test_helper'

class SubjectTest < Test::Unit::TestCase

# Tests validation failure using empty values
  def test_invalid_with_empty_values
    subject = Subject.new
    assert !subject.valid?
    assert subject.errors.invalid?(:name)
    assert subject.errors.invalid?(:size)
    assert subject.errors.invalid?(:description)
  end
  
# Tests validation using valid data
  def test_valid_subject
    subject = Subject.new(:name=>"Perl",
                          :size=>2,
                          :description=>"test",
                          :tag=>"123456789")
    assert subject.valid?
  end

# Tests invalid TAG length  
  def test_invalid_tag_length
    subject = Subject.new(:name=>"AJAX",
                          :size=>3,
                          :description=>"test",
                          :tag=>"abcdefghijklm")
    assert !subject.valid?
    assert_equal "is too long, maximum 10 characters",subject.errors.on(:tag)
  end 
end

如果以上单元测试成功,应该产生以下输出:


清单 14. 运行主题单元测试
                
D:\rails\teamroom>ruby test/unit/subject_test.rb
Loaded suite subject_test
Started
...
Finished in 13.891 seconds.

3 tests, 7 assertions, 0 failures, 0 errors

输出中的三个测试是指 subject_test.rb 文件中定义的三个测试,七个断言是指这个文件中的所有断言语句。正如前面在注 3 中提到的,“.” 是指成功的结果。

上面的 subject_test.rb 示例包含清单 15 所示的七个断言。


清单 15. subject_test.rb 中的断言
                
assert !subject.valid?
assert subject.errors.invalid?(:name)
assert subject.errors.invalid?(:size)
assert subject.errors.invalid?(:description)

assert subject.valid?

assert !subject.valid?
assert_equal "is too long, maximum 10 characters",subject.errors.on(:tag)

subject_test.rb 中的三个测试见清单 16。


清单 16. subject_test.rb 中的测试方法
                
test_invalid_with_empty_values
test_valid_subject
test_invalid_tag_length

用 fixture 进行测试

不需要在浏览器中手工输入值以测试 Rails 应用程序,而是可以使用测试 fixture 将示例数据装载到数据库表中。在运行测试之前,可以使用 fixture 用预定义的数据填充测试数据库。fixture 可以采用两种不同的格式:YAML 或逗号分隔的值(CSV)格式。因为 fixture 是独立于数据库的,所以可以使用同样的 fixture 针对不同的数据库系统测试 Rails 应用程序。

我们将使用 fixture 测试 Team Room 应用程序中的主题和订阅。对于我们的测试,使用一个 YAML fixture。为此,要编辑 /test/fixtures 目录中的 subjects.yml 文件。在 Rails 生成对应的单元测试时,它会创建一个空的 subjects.yml。当运行 ruby script/generate model 创建新模型,或运行 ruby script/generate scaffold 生成 scaffold 时,会自动生成 fixture 存根并放在 /test/fixtures 目录中。

我们编辑 subjects.yml 并填充 24 个不同的主题。在本文的 下载 一节提供的示例代码中可以找到 /test/fixtures/subject.yml。每个 fixture 有一个名称,后面是冒号分隔的键 - 值对。

subjects.yml 的内容示例:


清单 17. subjects.yml 的内容示例
                
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
photography:
  id: 1
  name: Photography
  size: 0
  description: Digital Photos, images
  tag: portraits

gardening:
  id: 2
  name: Gardening
  size: 0
  description: Gardening info
  tag: flowers

golf:   
  id: 3
  name: Golf
  size: 0
  description: Golf information, golf club, golf course discount available to members
  tag: golf

tennis:
  id: 4
  name: Tennis
  size: 0
  description: Tennis information, tennis club, tennis court bookings
  tag: tennis 


Rails fixture 装载机制已经就位了,所以不需要添加额外的代码来装载测试数据。我们修改了清单 13 中的 /test/unit/subject_test.rb,添加了用 fixture 进行测试的代码。


清单 18. /test/unit/subject_test.rb 中与 fixture 相关的条目
                
require File.dirname(__FILE__) + '/../test_helper'

class SubjectTest < Test::Unit::TestCase
  fixtures :subjects

  # count the number of subject fixtures
  def test_subject_fixtures
    assert_equal 24, Subject.count
  end
  ...  
  # Previous test methods and assertions are listed below
  ...
end

在测试用例中的每个测试方法的开头,fixtures 方法自动地装载与模型名对应的 fixture。在默认情况下,:subjects 让 Rails 从 subjects.yml 文件装载示例数据。然后,添加一个 “test_subject_fixtures” 方法,测试所有 24 个主题是否都正确地装载了。

发出 ruby test/unit/subject_test.rb 命令运行测试。输出如下:


清单 19. 使用 fixture 的主题单元测试输出
                
D:\rails\teamroom>ruby test/unit/subject_test.rb
Loaded suite test/unit/subject_test
Started
....
Finished in 3.15 seconds.

4 tests, 8 assertions, 0 failures, 0 errors

假设启动了 WEBrick 服务器并作为已注册的用户登录,现在浏览到 http://localhost:3000/subjects/list。您会注意到,现在已经可以订阅 24 个主题。


图 2. 填充的主题条目
主题条目

有时候,可能需要通过 ERb(嵌入式 Ruby)使用动态 fixture 生成 fixture 数据。一个非常常见的例子是在运行测试时生成实际的时间戳。可以使用 执行 Ruby 代码,使用 执行 Ruby 代码并显示结果。在后面的小节中解释功能测试和性能测试时,将会学习如何使用动态 fixture 和 ERb。

共享 fixture 并测试多对多关联

我们可以更进一步,使用 fixture 测试主题和订阅之间的关联。当然,可以浏览到 http://localhost:3000/subjects/list 并检查当前登录用户订阅的主题。然后,测试 Team Room 主题 - 订阅关联。但是,如果要测试许多用户订阅,涉及许多主题,那么这个任务就会非常繁琐。这意味着必须手工创建许多用户订阅,然后作为不同的用户登录,检查每个用户订阅的主题。

测试模型之间的多对多关系的有效方法是创建一个新的 fixture,其中包含联结表中的数据。在 第 2 部分 中,我们创建了 SUBJECTS_SUBSCRIPTIONS 表以表示 SUBJECTS 和 SUBSCRIPTIONS 表之间的多对多关系,并确保数据库是规范化的。现在编辑 /test/fixtures/subjects_subscriptions.yml 文件,用它将数据填充到 SUBJECTS_SUBSCRIPTIONS 表中:


清单 20. subjects_subscriptions.yml
                
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
# Refer to /test/fixtures/subjects.yml for more information
# Subject ID 2 = Gardening
subscription160_gardening:
  subscription_id: 160
  subject_id: 2
 
# Subject ID 3 = Golf  
subscription160_golf:
  subscription_id: 160
  subject_id: 3

这些数据意味着订阅 ID 为 160 的用户订阅了两个主题:Gardening 和 Golf。

要想测试主题和订阅的关联,就必须访问 SUBJECTS 和 SUBSCRIPTIONS 表以及联结表 SUBJECTS_SUBCRIPTIONS 中的数据。因此,在 /test/unit/subjects_subscription_test.rb 测试中,要装载这三个 fixture。


清单 21. fixture
                
class SubjectsSubscriptionTest < Test::Unit::TestCase
   fixtures :subjects_subscriptions, :subjects, :subscriptions
   [...]
end   

这说明 fixture 非常强大,让我们可以跨多个测试用例共享测试数据。关于 fixture 的更多信息,参见 Ruby on Rails 文档中的 Class Fixtures

使用 mock 对象

mock 可以帮助我们忽略外部系统的影响,专心测试应用程序的核心功能。在用户向 Team Room 应用程序注册时,我们检查了电子邮件地址是否具有正确的格式,但是我们不知道提供的电子邮件地址是否是实际存在的。实际上,确认用户电子邮件地址的惟一方法是,向这个电子邮件地址发送电子邮件。如果电子邮件地址无效或不存在,就会从电子邮件提供商那里收到一个错误。为了测试这个失败的订阅通知发送场景,可以使用 mock。

例如,我们可以在测试环境中用 mock 模拟电子邮件发送方法。我们在 /test/mocks/test 目录中创建一个 email_provider.rb 文件,它定义我们要模拟的发送方法。mock 文件就位之后,Rails 首先装载 /test/mocks/test/email_provider.rb,然后查看 /app/models/email_provider.rb。

/test/mocks/test/email_provider.rb 像下面这样:


清单 22. mock
                
require 'models/email_provider'
 
class EmailProvider
  def deliver(request)
    #Mock method
    :success
  end
end

mock 让我们能够集中精力测试 Team Room 应用程序的核心功能,而不需要为外部资源操心。

功能测试:对控制器进行测试

对 Rails 应用程序控制器进行测试的目的是,确保来自 Web 浏览器的所有请求都根据用户的输入得到了符合预期的响应,并产生适当的状态修改。所以,对于控制器中的每个动作,至少需要一个测试特定路径(URL)的测试用例,有时候可以用多个测试测试不同状态下的情况(例如,在用户已经通过身份验证和没有通过身份验证两种状态下,访问一个动作)。

为了体会一下功能测试,我们来考虑登录和注册动作涉及的 UsersController。在生成模型和控制器类时生成了相关联的 UsersControllerTest,它已经包含完整的设置模板和存根方法,只需稍加定制就能够处理执行路径。


清单 23. 用户控制器测试
                
require File.dirname(__FILE__) + '/../test_helper'
require 'users_controller'

# Re-raise errors caught by the controller.
class UsersController; def rescue_action(e) raise e end; end

class UsersControllerTest < Test::Unit::TestCase
  fixtures :users

  def setup
    @controller = UsersController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new

    @first_id = users(:first).id
  end
  [...]
end

基本方法 setup 对功能测试都需要的三个实例变量进行初始化:controller(要测试的控制器)、request(包含用户输入和 HTTP 头信息)和 response(包含来自模板的数据和状态码)。

对控制器进行测试的一个关键方面是相关联的 fixture,这个 fixture 为场景提供测试数据。这个控制器让我们有机会了解 fixture 中的一些动态特性的用法。在上面的测试用例中,提供一个已经通过身份验证的用户,需要用有效的数据填充 USERS 表,以便计算加密的密码。尽管加密的密码可以单独计算,而且只在 fixture 中直接使用,但是视图中使用的模板替换和部分工作是相似的,这会简化我们的任务。


清单 24. 用户 fixture
                

first:
  id: 141
  usertype:  usr
  firstname: Homer
  lastname:  Simpson
  extension: '9955'
  email:     homer@simpson.org
  userid:    homers
  salt: 
  hash_passwd:  

最初的 test_index 测试用例需要分割为两个不同的测试用例,它们分别测试用户已经通过身份验证和没有通过身份验证两种情况下的请求处理:


清单 25. 测试用户控制器中的索引动作
                
  def test_index_without_user
    get :index
    assert_redirected_to :action => "login"
    assert_equal "Please sign-in" , flash[:notice]
  end

  def test_index_with_user
    get :index, {}, {:user_id => users(:first).id}
    assert_response 302
  end

为了测试这个控制器的索引动作(如果用户还没有通过身份验证,就重定向到登录页面),这个测试用例需要模拟来自浏览器的 GET 请求。为了生成 HTTP 请求,ActionController::Integration::Session 定义的 get 方法接受以下参数:URL、代表随动作传递的 HTTP 参数的散列值(在上例中为空)和填充会话所需的参数的散列值。现在我们只需要调用特定的测试用例:


清单 26. 运行用户控制器测试
                
D:\rails\teamroom>ruby test\functional\users_controller_test.rb -n test_index_with_user
Loaded suite test/functional/users_controller_test
Started
.
Finished in 0.719 seconds.
1 tests, 1 assertions, 0 failures, 0 errors

测试电子邮件通知和订阅确认

对电子邮件通知进行测试涉及单元测试(确保生成的电子邮件内容有效)和功能测试(确保在预期的时间发送电子邮件通知)。在 第 2 部分 中,基于 Action Mailer 实现了电子邮件通知控制器;对于这个控制器,应该找到生成的功能测试存根 SubscriptionMailerTest 并利用为通知动作所涉及的模型创建的 fixture。

为了对电子邮件通知和订阅确认进行单元测试,邮件器测试驱动要求为本地电子邮件发送配置 ActionMailer(delivery_method = :test),还要使用模型 fixture 设置调用通知和确认所需的对象。


清单 27. 邮件器功能测试设置
                
class SubscriptionMailerTest < Test::Unit::TestCase
  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
  CHARSET = "utf-8"

  include ActionMailer::Quoting
  fixtures :users
  fixtures :subscriptions
  fixtures :subjects
  fixtures :documents

  def setup
    ActionMailer::Base.delivery_method = :test
    ActionMailer::Base.perform_deliveries = true
    ActionMailer::Base.deliveries = []

    @user = users(:first)
    @subscription = subscriptions(:one)
    @doc  = documents(:first)
    @subject = subjects(:ruby_on_rails)
    @subject.documents << @doc
    @subject.subscriptions << @subscription
  end
  [...]
end

然后,测试实现使用邮件器类创建电子邮件内容(create_notify),但是并不向 SMTP 服务器实际发送邮件,并通过使用断言检验生成的消息部分(例如,收件人、发件人、时间戳等等),包括消息体的某些部分。


清单 28. 通知电子邮件测试
                
  def test_notify
    response = SubscriptionMailer.create_notify(@doc)

    assert_equal("TeamRoom new shared document notification", response.subject)
    assert_equal("teamroom@developerWorks.ibm.com", response.from[0])
    assert_equal(nil, response.to)
    assert_equal("homer@simpson.org", response.bcc[0])
    assert_in_delta(Time.now, response.date, 1.0)
    assert_match(/A new document DB2onRails-logo.gif/, response.body)
    assert_match(/subject: Ruby on Rails/, response.body)
  end

使用相似的方式测试订阅确认电子邮件,创建并检验消息的内容。另外,邮件器的功能测试需要一个控制器测试用例,这个测试用例不发送电子邮件,而是将电子邮件消息追加到一个内存数组中(ActionMailer::Base.deliveries = [])。这样就可以检验发送的电子邮件数量,还可以检验每个电子邮件消息的内容。

性能测试

尽管在默认情况下在 test 目录中没有生成性能测试目录,但是开发人员可能希望评估一些应用程序性能指标。尽管在开发过程中太早关注性能是不明智的,但是当开发过程接近结束时,应该考虑一些性能场景来制定容量规划;在后续开发迭代中如果应用程序变慢了,应该进行一些回归测试。

为了讲解如何实现性能测试,我们来考虑一个场景:我们把 Team Room 应用程序部署在一个相当大的企业环境中,它可能承受过大的负载。我们像以前一样先设置一些测试数据,为此提供一组 fixture,这些 fixture 只在进行性能测试时装载,用来模拟对大量测试数据进行初始化时的情况。


清单 29. 用于大型测试数据集的另一个动态用户 fixture
                

user_:
  id: 
  usertype:  usr
  firstname: New
  lastname:  One
  extension: '9999'
  userid:    new
  email:     new@domain.net
  salt:      
  hash_passwd: 


可以根据常规的使用场景考虑性能测试,然后扩展到模拟某些极端负载的大型测试数据集。这意味着这个测试与常规功能测试非常相似,但是测试用例在某个方面很极端。我们来考虑如何测试用户控制器和登录动作在极端负载下的表现。除了装载测试数据的位置(self.fixture_path)不同之外,测试用例的设置应该是相同的。


清单 30. 用户登录性能测试设置
                
class UserLoginTest < Test::Unit::TestCase
  self.fixture_path = Pathname.new(File.dirname(__FILE__)).parent + 
                                   'fixtures' + 'performance'
  fixtures :users

  def setup
    @controller = UsersController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end
  [...]
end

现在,为了测试 Team Room 应用程序处理大量并发用户的能力,可以尝试让 fixture 中定义的所有用户同时登录,甚至可以为这些动作指定时间限制。在清单 31 中,我们用一个循环实现这种性能测试,然后关闭登录器,使用 Ruby 标准库基准收集时间估计值:


清单 31. 测试用户控制器在登录动作期间的性能
                
  def test_1000_login
    @controller.logger.silence do
      user_count = 0
      elapsed_time = Benchmark.realtime do
        1200.downto(200) do |id|
          user = users("user_#{id}")
          post :login, :userid => user.userid, :password => 'secret'
          user_count += 1
        end
      end
      assert_equal 1001, user_count
      assert elapsed_time < 10.0
    end
  end

再考虑一个更真实的性能场景:生成市场营销报告时,要执行一系列 XML 查询。我们把这个问题留给读者作为练习;如果读者希望把这个应用程序或相似的应用程序投入生产环境,就需要考虑这个问题。性能测试实现的一个重要特点是,使用常规的功能性和集成测试用例,但是采用大型测试数据集和沉重的负载。这种方式使性能回归测试的实现非常容易。

使用 Rake 运行应用程序测试

在使用 rails teamroom 命令开始创建 Team Room 应用程序时,生成了一个目录结构;更重要的是,还 “挂上” 了 Rails gem 内置的一组实用程序。Rails gem 是一个相当简单的组件,它在幕后无缝地集成所有其他组件;最明显的两个组件是 Active RecordAction View。同时,Rails gem 还通过强大的 Rake gem helper 使用框架解决所有 Web 应用程序共有的一些问题。

Rake 在 Ruby 中的作用相当于 make 实用程序。它通过预定义的任务(相当于 make 目标),为迁移、模式装载/转储、Rails gem 更新/冻结和文档创建提供快捷操作,还支持测试。在 Team Room 应用程序根目录中运行 rake --tasks ,就可以看到 rake 提供的所有预定义任务。这些任务从一开始(在创建目录结构和 Rake 文件时)就已经注入 Team Room 应用程序中,然后共享 tasks/rails 库路径(这里包含预定义任务集)。

只需使用简单的 rake test 命令,就可以一起执行到目前为止构建的所有单元测试和功能测试;在开发周期中的任何时候都能够轻松地执行测试。还可以按类别运行测试;由于 Rails 应用程序采用标准的目录结构,所以很容易装载 test/unit 路径上定义的所有测试,见清单 32


清单 32. 运行 Team Room 应用程序的所有单元测试
                
D:\rails\teamroom>rake test:units
(in D:/rails/teamroom)
D:/ruby/bin/ruby -Ilib;test 
"D:/ruby/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader.rb" 
"test/unit/customer_info_test.rb" "test/unit/document_test.rb" 
"test/unit/subjects_subscription_test.rb" "test/unit/subject_test.rb" 
"test/unit/subscription_mailer_test.rb" "test/unit/subscription_test.rb" 
"test/unit/user_test.rb" "test/unit/xml_content_test.rb"
Loaded suite D:/ruby/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader
Started
..........
Finished in 11.553 seconds.

调用 rake test:functionals 时会执行所有功能测试,rake 会把测试串联起来,覆盖整个测试集。

DB2 Rails 应用程序的问题诊断

在开发 Rails 应用程序时,可能会遇到出乎意料的结果或错误。在这种情况下,首先要检查 /log 目录(在我们的示例中,是 d:\rails\teamroom\log)中的日志文件。Rails 将每个环境的日志消息和错误写入它自己的日志。对于开发环境中的问题,应该查看 development.log;对于测试环境中的问题,应该查看 test.log。除了查看 Rails 日志并在应用程序本身中诊断问题之外,还可以使用 DB2 跟踪实用程序和日志进一步诊断 Rails 应用程序的问题。

CLI 跟踪

正如本系列的 第 1 部分 中提到的,ibm_db 驱动程序利用 IBM driver for Open Database Connectivity(ODBC) and Call Level Interface(CLI)连接 IBM 数据服务器。这意味着与 IBM 数据服务器的所有交互都要通过 IBM driver for ODBC and CLI,可以使用 CLI 跟踪捕获这些交互。CLI 跟踪捕获 Ruby 应用程序向 DB2 driver for ODBC and CLI 发出的所有 API 调用,并在日志中记录所有输入参数以及驱动程序返回给应用程序的值。因此,可以利用 CLI 跟踪检查输入和输出值,以及 Rails 框架在幕后生成的 SQL 语句。

Infocenter 可以找到启用 CLI 跟踪所需的步骤。

对于检查应用程序发出的 SQL 语句和数据库返回的值,CLI 跟踪是非常有价值的工具。在 Team Room 应用程序中,大多数 SQL 语句是 Rails 生成的。下面的 CLI 跟踪输出显示在上载 XML 文档(见 图 2)时发送给 DB2 数据库的查询。可以看出,上载 XML 文档涉及在三个表中插入数据。

  1. 将内容类型、名称、平台、大小、主题、上载这个文件的用户的 id 以及更新和创建时间插入 documents 表。



    清单 33. CLI 跟踪:第 1 部分
                            
    SQLExecDirect( hStmt=1:8 )
        ---&gt Time elapsed - +1.844000E-003 seconds
    ( pszSqlStr="INSERT INTO documents (content_type, name, platform, size, updated_at, 
    subject_id, user_id, created_at, data) VALUES('text/xml', 'CAN-Central.xml', 'Any', 
    125177, '2007-06-07 13:12:57', NULL, 100, '2007-06-07 13:12:49', NULL)", cbSqlStr=225 )
    ( StmtOut="INSERT INTO documents (content_type, name, platform, size, updated_at, 
    subject_id, user_id, created_at, data) VALUES(?, ?, ?, 125177, ?, NULL, 100, ?, NULL)" )
    ( Package="SYSSH200          ", Section=11 )
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 349 )
        sqlccsend( ) rc - 0, time elapsed - +1.991000E-003
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 188 ) - rc - 0, time elapsed - +2.471000E-003
    ( Row=1, iPar=1, fCType=SQL_C_CHAR, rgbValue="text/xml" - x'746578742F786D6C', pcbValu...
    ( Row=1, iPar=2, fCType=SQL_C_CHAR, rgbValue="CAN-Central.xml" - x'43414E2D43656E74726...
    ( Row=1, iPar=3, fCType=SQL_C_CHAR, rgbValue="Any" - x'416E79', pcbValue=3, piIndicato...
    ( Row=1, iPar=4, fCType=SQL_C_CHAR, rgbValue="2007-06-07 13:12:57" - x'323030372D30362...
    ( Row=1, iPar=5, fCType=SQL_C_CHAR, rgbValue="2007-06-07 13:12:49" - x'323030372D30362...
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 223 )
        sqlccsend( ) rc - 0, time elapsed - +2.380000E-004
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 127 ) - rc - 0, time elapsed - +1.909000E-003
    
    SQLExecDirect( )
        

  2. 插入 XML 文件的名称(与 documents 表中的 id 对应)和 XML 占位文档。



    清单 34. CLI 跟踪:第 2 部分
                            
    SQLExecDirect( hStmt=1:8 )
        ---&gt Time elapsed - +5.003000E-003 seconds
    ( pszSqlStr="INSERT INTO xml_contents (name, document_id, data) VALUES('CAN-Central.xml',
     101, '@@@IBMXML@@@')", cbSqlStr=108 )
    ( StmtOut="INSERT INTO xml_contents (name, document_id, data) VALUES(?, 101, ?)" )
    ( Package="SYSSH200          ", Section=11 )
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 261 )
        sqlccsend( ) rc - 0, time elapsed - +1.799000E-003
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 137 ) - rc - 0, time elapsed - +1.865000E-003
    ( Row=1, iPar=1, fCType=SQL_C_CHAR, rgbValue="CAN-Central.xml" - x'43414E2D43656E747261...
    ( Row=1, iPar=2, fCType=SQL_C_CHAR, rgbValue="@@@IBMXML@@@" - x'3C69626D3E40...
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 221 )
        sqlccsend( ) rc - 0, time elapsed - +1.000000E-005
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 89 ) - rc - 0, time elapsed - +1.840000E-003
    
    SQLExecDirect( )
        

  3. 然后是一个更新语句,它实际插入 XML 数据。



    清单 35. CLI 跟踪:第 3 部分
                            
    SQLPrepare( hStmt=1:8 )
        ---&gt Time elapsed - +1.861000E-003 seconds
    ( pszSqlStr="UPDATE xml_contents SET (data) = (?) WHERE id = 101", cbSqlStr=51 )
    ( StmtOut="UPDATE xml_contents SET (data) = (?) WHERE id = 101" )
    ( Package="SYSSH200          ", Section=11 )
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 244 )
        sqlccsend( ) rc - 0, time elapsed - +1.000000E-005
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 120 ) - rc - 0, time elapsed - +1.923000E-003
    
    SQLPrepare( )
        

  4. 最后,使用插入语句相应地填充 SUBJECTS 表。



    清单 36. CLI 跟踪:第 4 部分
                            
    SQLExecDirect( hStmt=1:10 )
        ---&gt Time elapsed - +2.166000E-003 seconds
    ( pszSqlStr="INSERT INTO subjects (name, size, tag, description) VALUES('Marketing', 1, 
    'sales', '@@@IBMTEXT@@@')", cbSqlStr=100 )
    ( StmtOut="INSERT INTO subjects (name, size, tag, description) VALUES(?, 1, ?, ?)" )
    ( Package="SYSSH200          ", Section=14 )
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 263 )
        sqlccsend( ) rc - 0, time elapsed - +1.920000E-004
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 154 ) - rc - 0, time elapsed - +1.823000E-003
    ( Row=1, iPar=1, fCType=SQL_C_CHAR, rgbValue="Marketing" - x'4D61726B6574696E67', ...
    ( Row=1, iPar=2, fCType=SQL_C_CHAR, rgbValue="sales" - x'73616C6573', pcbValue=5, ...
    ( Row=1, iPar=3, fCType=SQL_C_CHAR, rgbValue="@@@IBMTEXT@@@" - x'40404049424D5445 ...
        sqlccsend( Handle - 84681200 )
        sqlccsend( ulBytes - 182 )   
        sqlccsend( ) rc - 0, time elapsed - +3.160000E-004
        sqlccrecv( timeout - +0.000000E+000 )
        sqlccrecv( ulBytes - 89 ) - rc - 0, time elapsed - +2.128700E-002
    
    SQLExecDirect( )
        

DB2 跟踪

DB2 跟踪捕获所有可跟踪的内部 DB2 函数调用,可以用来诊断涉及 DB2 的应用程序问题。它提供应用程序执行时 DB2 内部活动的相关信息。DB2 跟踪的一些性质对于收集信息很有帮助。

  1. 可以动态地打开和关闭跟踪。不需要重新启动应用程序就能够启用和禁用跟踪。这意味着,如果知道准确的故障点,就可以在故障点之前启用跟踪,从而只收集相关的信息。
  2. DB2 跟踪信息可以存储在内存或磁盘中。
  3. 可以限制跟踪的范围,只跟踪特定的组件。

关于启用 DB2 跟踪的说明,请参考 Infocenter

当调试 Rails 应用程序的 DB2 问题时,在运行应用程序的客户机上同时收集 DB2 跟踪和 CLI 跟踪会很有帮助。

db2diag.log 和 db2diag 工具

db2diag.log 记录 DB2 遇到的错误和一些警告。通过查看 db2diag.log,可以了解在应用程序执行期间是否有任何 DB2 错误和警告。可以通过 dbm cfg 配置日志的敏感度,默认设置是 3。

对于 DB2 9®,可以使用分析工具 db2diag 对 db2diag.log 文件进行过滤和格式化。可以用它获得只针对特定数据库或时间戳值的日志记录。关于 db2diag 工具的更多信息,请参考 Infocenter

常见问题和提示

  1. 为了使用 ibm_db Ruby 适配器和驱动程序,是否需要安装 DB2 客户机?

    是的。正如本系列的 第 1 部分 中指出的,IBM_DB 适配器(ibm_db_adapter.rb)直接依赖于 ibm_db Ruby 驱动程序,ibm_db Ruby 驱动程序利用 IBM driver for ODBC and CLI 连接 IBM 数据服务器。因此,至少需要 IBM DB2 Driver for ODBC and CLI,但是任何 DB2 9 FP2 或更高的客户机包(包含 CLI 驱动程序)都足以支持 IBM_DB 适配器连接所有 IBM 数据服务器。

  2. 在对 DB2 运行 Rails 应用程序时,遇到了以下错误:

    SQL0954C: Not enough storage is available in the application heap to process the statement.

    正如 第 1 部分 中指出的,针对 DB2 9 的 Rails 应用程序需要的最小 APPLHEAPSZ 是 1024。连接您的数据库并获取配置参数,检查 APPLHEAPSZ。

  3. 要想以 IBM Ruby Driver 方式访问 DB2 i5 或 DB2 for z/OS 服务器,需要 DB2 Connect 吗?

    是的,需要有 DB2 Connect,DB2 客户机才能连接 DB2 i5 或 DB2 for z/OS 服务器。在使用 IBM DB2 driver for ODBC and CLI 时,在驱动程序安装路径中需要一个有效的许可文件。

  4. 在尝试用 rake db:test:* 命令克隆出与开发环境相同的测试环境时,遇到了 rake aborted 错误。

    检查 database.yml 文件。需要根据 database.yml 设置测试环境,database.yml 是 rails 命令生成的默认配置。另外,一定要按照注 1 中的说明更新两个文件。

  5. 遇到了在 IBM_DB 适配器的最新版本中已经修复的问题。但是,gem list --local 命令显示已经安装了 IBM_DB 适配器的最新版本。

    确保在 \lib\ruby\gems\1.8\gems\activerecord-1.15.3\lib\active_record\connection_adapters 或 UNIX® 上的相似路径中没有 ibm_db_adapter.rb 的副本。

    最新版本 IBM_DB gem 中的 ibm_db_adapter.rb 应该只安装在 GEM_HOME 路径 \lib\ruby\gems\1.8\gems\ibm_db--mswin32\lib\active_record\connection_adapters(或 UNIX 上的相似路径)中。这是装载到 Rails 环境中的唯一 IBM_DB 适配器。

  6. 在即将发布的 Rails 1.2.4 中,将会取消 config.connection_adaptersRAILS_CONNECTION_ADAPTERS。因此,不再需要按照 第 1 部分 中描述的手工步骤在 Rails 框架中的连接适配器列表中注册 “ibm_db”。使用 gem install ibm_db 命令安装 IBM_DB 适配器之后,Rails 环境马上就能够找到并装载它。

结束语

Rails 框架内置的测试支持让测试工作变得轻松多了!每个 Rails 应用程序都在 config/database.yml 文件中定义了测试、开发和生产环境,因此可以为不同的用途设置不同的数据库。在最初创建新的 Rails 项目时,Rails 会替我们生成测试基础设施。对于您创建的每个模型和控制器,都会创建对应的测试存根。对 Rails 模型进行单元测试,然后通过功能测试和集成测试在更高层次上确保 Rails 应用程序符合设计预期。可以使用 fixture 指定测试用的数据;mock 对象让我们能够集中精力测试应用程序的核心功能,而不必为网络连接或外部系统操心。这些特性都是 Rails 内置的,这让测试变得非常方便。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/9403012/viewspace-90/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/9403012/viewspace-90/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值