1 为什么要为 Rails 程序编写测试?
在 Rails 中编写测试非常简单,生成模型和控制器时,已经生成了测试代码骨架。
即便是大范围重构后,只需运行测试就能确保实现了所需功能。
Rails 中的测试还可以模拟浏览器请求,无需打开浏览器就能测试程序的响应。
2 测试简介
测试是 Rails 程序的重要组成部分,不是处于尝鲜和好奇才编写测试。基本上每个 Rails 程序都要频繁和数据库交互,所以测试时也要和数据库交互。为了能够编写高效率的测试,必须要了解如何设置数据库以及导入示例数据。
2.1 测试环境
默认情况下,Ra
2.2 Rails Sets up for Testing from the Word Go
执行 rails new
命令生成新程序时,Rails 会创建一个名为 test
的文件夹。这个文件夹中的内容如下:
$ ls -F test
controllers/ helpers/ mailers/ test_helper.rb
fixtures/ integration/ models/
|
modles
文件夹存放模型测试,controllers
文件夹存放控制器测试,integration
文件夹存放多个控制器之间交互的测试。
fixtures
文件夹中存放固件。固件是一种组织测试数据的方式。
test_helper.rb
文件中保存测试的默认设置。
config/database.yml
文件中设置。
测试使用的数据库独立于其他环境,不会影响开发环境和生产环境的数据库。
2.3 固件详解
好的测试应该应该具有提供测试数据的方式。在 Rails 中,测试数据由固件提供。
2.3.1 固件是什么?
固件代指示例数据,在运行测试之前,把预先定义好的数据导入测试数据库。固件相互独立,一个文件对应一个模型,使用 YAML 格式编写。
固件保存在文件夹 test/fixtures
中,执行 rails generate model
命令生成新模型时,会在这个文件夹中自动创建一个固件文件。
2.3.2 YAML
使用 YAML 格式编写的固件可读性极高,文件的扩展名是 .yml
,例如 users.yml
。
下面举个例子:
# lo & behold! I am a YAML comment!
david:
name: David Heinemeier Hansson
birthday: 1979-10-15
profession: Systems development
steve:
name: Steve Ross Kellock
birthday: 1974-09-27
profession: guy with keyboard
|
每个附件都有名字,后面跟着一个缩进后的键值对列表。记录之间往往使用空行分开。在固件中可以使用注释,在行首加上 #
符号即可。如果键名使用了 YAML 中的关键字,必须使用引号,例如 'yes'
和 'no'
,这样 YAML 解析程序才能正确解析。
如果涉及到关联,定义一个指向其他固件的引用即可。例如,下面的固件针对 belongs_to/has_many
关联:
# In fixtures/categories.yml
about:
name: About
# In fixtures/articles.yml
one:
title: Welcome to Rails!
body: Hello world!
category: about
3 为模型编写单元测试
在 Rails 中,单元测试用来测试模型。
本文会使用 Rails 脚手架生成模型、迁移、控制器、视图和遵守 Rails 最佳实践的完整测试组件。我们会使用自动生成的代码,也会按需添加其他代码。
执行 rails generate scaffold
命令生成资源时,也会在 test/models
文件夹中生成单元测试文件:
$ rails generate scaffold post title:string body:text
...
create app/models/post.rb
create test/models/post_test.rb
create test/fixtures/posts.yml
...
|
test/models/post_test.rb
文件中默认的测试代码如下:
require
'test_helper'
class
PostTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
|
下面逐行分析这段代码,熟悉 Rails 测试的代码和相关术语。
require
'test_helper'
|
现在你已经知道,test_helper.rb
文件是测试的默认设置,会载入所有测试,因此在所有测试中都可使用其中定义的方法。
class
PostTest < ActiveSupport::TestCase
|
PostTest
继承自 ActiveSupport::TestCase
,定义了一个测试用例,因此可以使用 ActiveSupport::TestCase
中的所有方法。后文会介绍其中一些方法。
MiniTest::Unit::TestCase
(ActiveSupport::TestCase
的父类)子类中每个以 test
开头(区分大小写)的方法都是一个测试,所以,test_password
、test_valid_password
和 testValidPassword
都是合法的测试名,运行测试用例时会自动运行这些测试。
Rails 还提供了 test
方法,接受一个测试名作为参数,然后跟着一个代码块。test
方法会生成一个 MiniTest::Unit
测试,方法名以 test_
开头。例如:
test
"the truth"
do
assert
true
end
|
和下面的代码是等效的
def
test_the_truth
assert
true
end
|
不过前者的测试名可读性更高。当然,使用方法定义的方式也没什么问题。
assert
true
这行代码叫做“断言”(assertion)。断言只有一行代码,把指定对象或表达式和期望的结果进行对比。例如,断言可以检查:
- 两个值是够相等;
- 对象是否为
nil
; - 这行代码是否抛出异常;
- 用户的密码长度是否超过 5 个字符;
每个测试中都有一个到多个断言。只有所有断言都返回真值,测试才能通过。
运行测试
运行测试执行 rake test
命令即可,在这个命令中还要指定要运行的测试文件。
$ rake test test/models/post_test.rb
.
Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
上述命令会运行指定文件中的所有测试方法。注意,test_helper.rb
在 test
文件夹中,因此这个文件夹要使用 -I
旗标添加到加载路径中。
还可以指定测试方法名,只运行相应的测试。
$ rake test test/models/post_test.rb test_the_truth
.
Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
上述代码中的点号(.
)表示一个通过的测试。如果测试失败,会看到一个 F
。如果测试抛出异常,会看到一个 E
。输出的最后一行是测试总结。
要想查看失败测试的输出,可以在 post_test.rb
中添加一个失败测试。
test
"should not save post without title"
do
post = Post.
new
assert_not post.save
end
|
我们来运行新添加的测试:
$ rake test test/models/post_test.rb test_should_not_save_post_without_title
F
Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s.
1) Failure:
test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
Failed assertion, no message given.
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
|
在输出中,F
表示失败测试。你会看到相应的调用栈和测试名。随后还会显示断言实际得到的值和期望得到的值。默认的断言消息提供了足够的信息,可以帮助你找到错误所在。要想让断言失败的消息更具可读性,可以使用断言可选的消息参数,例如:
test
"should not save post without title"
do
post = Post.
new
assert_not post.save,
"Saved the post without a title"
end
|
运行这个测试后,会显示一个更友好的断言失败消息:
1) Failure:
test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
Saved the post without a title
|
如果想让这个测试通过,可以在模型中为 title
字段添加一个数据验证:
class
Post < ActiveRecord::Base
validates
:title
, presence:
true
end
|
现在测试应该可以通过了,再次运行这个测试来验证一下:
$ rake test test/models/post_test.rb test_should_not_save_post_without_title
.
Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
你可能注意到了,我们首先编写一个检测所需功能的测试,这个测试会失败,然后编写代码,实现所需功能,最后再运行测试,确保测试可以通过。这一过程,在软件开发中称为“测试驱动开发”(Test-Driven Development,TDD)。
要想查看错误的输出,可以在测试中加入一处错误:
test
"should report error"
do
# some_undefined_variable is not defined elsewhere in the test case
some_undefined_variable
assert
true
end
|
运行测试,很看到以下输出:
$ rake test test/models/post_test.rb test_should_report_error
E
Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s.
1) Error:
test_should_report_error(PostTest):
NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0>
test/models/post_test.rb:10:in `block in <class:PostTest>'
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
|
注意上面输出中的 E
,表示测试出错了。
如果测试方法出现错误或者断言检测失败就会终止运行,继续运行测试组件中的下个方法。测试按照字母顺序运行。
测试失败后会看到相应的调用栈。默认情况下,Rails 会过滤调用栈,只显示和程序有关的调用栈。这样可以减少输出的内容,集中精力关注程序的代码。如果想查看完整的调用栈,可以设置 BACKTRACE
环境变量:
$ BACKTRACE=1 rake test test/models/post_test.rb
可用的断言
断言有很多种,下面列出了可在 Rails 默认测试库 minitest
中使用的断言。方法中的 [msg]
是可选参数,指定测试失败时显示的友好消息。
断言 | 作用 |
---|---|
assert( test, [msg] ) | 确保 test 是真值 |
assert_not( test, [msg] ) | 确保 test 是假值 |
assert_equal( expected, actual, [msg] ) | 确保 expected == actual 返回 true |
assert_not_equal( expected, actual, [msg] ) | 确保 expected != actual 返回 true |
assert_same( expected, actual, [msg] ) | 确保 expected.equal?(actual) 返回 true |
assert_not_same( expected, actual, [msg] ) | 确保 expected.equal?(actual) 返回 false |
assert_nil( obj, [msg] ) | 确保 obj.nil? 返回 true |
assert_not_nil( obj, [msg] ) | 确保 obj.nil? 返回 false |
assert_match( regexp, string, [msg] ) | 确保字符串匹配正则表达式 |
assert_no_match( regexp, string, [msg] ) | 确保字符串不匹配正则表达式 |
assert_in_delta( expecting, actual, [delta], [msg] ) | 确保数字 expected 和 actual 之差在 delta 指定的范围内 |
assert_not_in_delta( expecting, actual, [delta], [msg] ) | 确保数字 expected 和 actual 之差不在 delta 指定的范围内 |
assert_throws( symbol, [msg] ) { block } | 确保指定的代码块会抛出一个 Symbol |
assert_raises( exception1, exception2, ... ) { block } | 确保指定的代码块会抛出其中一个异常 |
assert_nothing_raised( exception1, exception2, ... ) { block } | 确保指定的代码块不会抛出其中一个异常 |
assert_instance_of( class, obj, [msg] ) | 确保 obj 是 class 的实例 |
assert_not_instance_of( class, obj, [msg] ) | 确保 obj 不是 class 的实例 |
assert_kind_of( class, obj, [msg] ) | 确保 obj 是 class 或其子类的实例 |
assert_not_kind_of( class, obj, [msg] ) | 确保 obj 不是 class 或其子类的实例 |
assert_respond_to( obj, symbol, [msg] ) | 确保 obj 可以响应 symbol |
assert_not_respond_to( obj, symbol, [msg] ) | 确保 obj 不可以响应 symbol |
assert_operator( obj1, operator, [obj2], [msg] ) | 确保 obj1.operator(obj2) 返回真值 |
assert_not_operator( obj1, operator, [obj2], [msg] ) | 确保 obj1.operator(obj2) 返回假值 |
assert_send( array, [msg] ) | 确保在 array[0] 指定的方法上调用 array[1] 指定的方法,并且把 array[2] 及以后的元素作为参数传入,该方法会返回真值。这个方法很奇特吧? |
flunk( [msg] ) | 确保测试会失败,用来标记测试还没编写完 |
Rails 使用的测试框架完全模块化,因此可以自己编写新的断言。Rails 本身就是这么做的,提供了很多专门的断言,可以简化测试。
Rails 提供的断言
Rails 为 test/unit
框架添加了很多自定义的断言:Rails adds some custom assertions of its own to the test/unit
framework:
断言 | 作用 |
---|---|
assert_difference(expressions, difference = 1, message = nil) {...} | 测试 expressions 的返回数值和代码块的返回数值相差是否为 difference |
assert_no_difference(expressions, message = nil, &block) | 测试 expressions 的返回数值和代码块的返回数值相差是否不为 difference |
assert_recognizes(expected_options, path, extras={}, message=nil) | 测试 path 指定的路由是否正确处理,以及 expected_options 指定的参数是够由 path 处理。也就是说 Rails 是否能识别 expected_options 指定的路由 |
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) | 测试指定的 options 能否生成 expected_path 指定的路径。这个断言是 assert_recognizes 的逆测试。extras 指定额外的请求参数。message 指定断言失败时显示的错误消息。 |
assert_response(type, message = nil) | 测试响应是否返回指定的状态码。可用 :success 表示 200-299,:redirect 表示 300-399,:missing 表示 404,:error 表示 500-599。状态码可用具体的数字表示,也可用相应的符号表示。详细信息参见完整的状态码列表,以及状态码数字和符号的对应关系。 |
assert_redirected_to(options = {}, message=nil) | 测试 options 是否匹配所执行动作的转向设定。这个断言可以匹配局部转向,所以 assert_redirected_to(controller: "weblog") 可以匹配转向到 redirect_to(controller: "weblog", action: "show") 等。还可以传入具名路由,例如 assert_redirected_to root_path ,以及 Active Record 对象,例如 assert_redirected_to @article 。 |
assert_template(expected = nil, message=nil) | 测试请求是否由指定的模板文件渲染 |