ruby on rails 的模型测试

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 文件中保存测试的默认设置。


ils 程序有三个环境:开发环境,测试环境和生产环境。每个环境所需的数据库在 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::TestCaseActiveSupport::TestCase 的父类)子类中每个以 test 开头(区分大小写)的方法都是一个测试,所以,test_passwordtest_valid_passwordtestValidPassword 都是合法的测试名,运行测试用例时会自动运行这些测试。

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.rbtest 文件夹中,因此这个文件夹要使用 -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] )确保数字 expectedactual 之差在 delta 指定的范围内
assert_not_in_delta( expecting, actual, [delta], [msg] )确保数字 expectedactual 之差不在 delta 指定的范围内
assert_throws( symbol, [msg] ) { block }确保指定的代码块会抛出一个 Symbol
assert_raises( exception1, exception2, ... ) { block }确保指定的代码块会抛出其中一个异常
assert_nothing_raised( exception1, exception2, ... ) { block }确保指定的代码块不会抛出其中一个异常
assert_instance_of( class, obj, [msg] )确保 objclass 的实例
assert_not_instance_of( class, obj, [msg] )确保 obj 不是 class 的实例
assert_kind_of( class, obj, [msg] )确保 objclass 或其子类的实例
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, &amp;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)测试请求是否由指定的模板文件渲染

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值