Ruby on Rails 单元测试

为什么要写测试文件?

软件开发中,一个重要的环节就是编写测试文件,对代码进行单元测试,确保程序各部分功能执行正确。但是,这一环节很容易被我们轻视,认为进行单元测试的必要性不大,最主要的一个原因是需要耗费大量时间。显然,这种观点是很浅显的,Michael Hartl 在他的《Ruby on Rails 教程——通过 Rails 学习 Web 开发》中指出编写自动化测试主要有三个好处:

  1. 测试能避免回归(regression)问题,即由于某些原因之前能用的功能不能用了;
  2. 有测试在,重构(改变实现方式,但功能不变)时更有自信;
  3. 测试是应用代码的客户,因此可以协助我们设计,以及决定如何与系统的其他组件交互。

也就是说,我们也许能够保证第一遍书写的代码的正确性,但我们难以保证日后对代码进行数次迭代更新,优化之后,它仍然能够保持最初的正确性。因为随着设计的不断深入,我们应对的情况会越来越复杂,这时候单元测试的重要性就显得尤为重要,代码的正确性很多时候就要通过单元测试来维持。

何时进行测试,何时不需要测试?

同上,Michael 在他的 Rails 教程中指出:

  • 与应用代码相比,如果测试代码特别简短,倾向于先编写测试
  • 如果对想实现的功能不是特别清楚,倾向于先编写应用代码,然后再编写测试,并改进实现方式
  • 安全是头等大事,保险起见,要为安全相关的功能先编写测试
  • 只要发现一个问题,就编写一个测试重现这种问题,避免回归,然后再编写应用代码修正问题
  • 尽量不为以后可能修改的代码(例如 HTML 结构的细节)编写测试
  • 重构之前要编写测试,集中测试容易出错的代码。

以此次 Rails 开发为例,我们主要编写了控制器和模型测试文件。

Ruby on Rails 的模型测试

在 Rails 中编写测试非常方便,生成模型和控制器时,已经在 ./test 路径下生成了各种测试代码骨架。即便是大范围重构后,只需运行测试就能确保实现了所需功能。Rails 中的测试还可以模拟浏览器请求,无需打开浏览器就能测试程序的响应。

test 文件夹内容如下:

$ ls -F test
controllers/ helpers/ mailers/ test_helper.rb
fixtures/ integration/ models/

models 文件夹存放模型测试,controllers 文件夹存放控制器测试,integration 文件夹存放多个控制器之间交互的测试。fixtures 文件夹中存放固件,用于提供初始的测试数据,固件相互独立,一个文件对应一个模型,使用 YAML 格式编写。在运行测试之前,Rails 会把预先定义好的数据导入测试数据库。

test_helper.rb 文件中保存测试的默认设置。

以一次测试为例,test/controllers/club_management_controller_test.rb 中,测试新增的社团活动评价功能:

test 'should evaluate activity' do
# 根据fixtures中的yaml文件获取活动信息
activity = activities(:basketball)
# 定义评价信息
club = clubs(:basketball_club)
reason = 'nice'
suggestion = 'no suggestion'
# 向相应的路由post评价请求,将评价信息作为参数传递
put activity_evaluate_path(club_id: club.id, activity_id: activity.id),
params: { rank: 233, reason: reason, suggestion: suggestion }
# 断言HTML响应,200表示操作应成功
assert_response 200
puts JSON.parse(@response.body)['data']
# 模拟浏览器响应,应该与上面传递的信息一致
assert_equal 10, JSON.parse(@response.body)['data']['rank']
assert_equal reason, JSON.parse(@response.body)['data']['reason']
assert_equal suggestion, JSON.parse(@response.body)['data']['suggestion']
put activity_evaluate_path(club_id: club.id, activity_id: activity.id),
params: { rank: -8 }
assert_response 200
assert_equal 0, JSON.parse(@response.body)['data']['rank']
end

在此次测试中,activityclub 数据均已在 fixtures/*.yml 中定义好,fixtures/activities.yml 如下

basketball:
name: 篮球赛
start_time: 2100-04-13 03:04:48
end_time: 2100-04-13 03:04:48
act_date: 2020-05-20
beg_time: 2020-05-20 13:14:02
fin_time: 2020-05-20 13:14:10
position: MyString
description: MyText
max_people_limit: 1
need_enroll: true
review_state: 0

fixtures/clubs.yml 如下

basketball_club:
name: 篮球社
english_name: basketball_club
introduction: play basketball
level: 3
club_category: sport
is_related_to_wechat: false
tags_json: '{}'
has_activities_applying: 0
has_activities_unevaluated: 0

同时,需要定义社团和活动的所属关系,在 fixtures/club_to_activities.yml 中:

···
five:
club: basketball_club
activity: basketball

之所以需要定义这种关系,是因为活动不能架空,活动应该有其相应的举办社团。我们在对活动进行操作时,这两项是必不可少的,社团和活动都要存在。Rails 为控制器提供了这种逻辑检验方法 before_action ,在 app/controllers/club_management_controller.rb 中:

before_action :_find_club, only:
%i[club_profile club_update activity_index activity_create activity_profile activity_update activity_evaluate article_index article_create article_profile article_update activity_review]
before_action :_find_activity, only: %i[activity_profile activity_update activity_evaluate activity_review]
private def _find_activity
# 寻找活动的代码段
end
private def _find_club
# 寻找社团的代码段
end

这段代码的意义已经在英文命名中体现出来了,只有在进行 [···] 这些操作之前时才执行 find 方法,从而保证逻辑的正确性。这种检验方法也使得我们不用每一次都手动调用 find 方法再进行判断,方便快捷又清晰。

但是,现在还没有万事大吉,因为还没有登录,没有任何权限,无法进行操作。同上,Rails 在 test 中也为我们提供了 setup 方法,我们只需要重写此方法就可以实现每个 test 块之前的设置工作,setup 方法定义在 Ruby26-x64/lib/ruby/gems/2.6.0/gems/minitest-5.14.1/lib/minitest/test.rb 中定义(不同版本的 ruby 可能路径不同):

##
# Runs before every test. Use this to set up before each test
# run. def setup; end

app/controllers/club_management_controller.rb 中重写此方法进行管理员登录:

def setup
@user = users(:three)
post session_login_path, params:
{
username: @user.email,
password: 'password'
}
end

其中 fixtures/users.yml 文件如下:

three:
username: zkk
nickname: nick
real_name: zkr
student_id: 16029999
tag: abcd
role: 3
password_digest: <%= User.digest('password') %>
remember_digest: <%= User.digest('password') %>
open_id: 3
union_id: 3
wxid: c3
session_key: 1001
college_id: 1
email: 787797770@qq.com
political_status_id: 1
gender: 1
phone_number: 1334234321
id_number: 610104199803281547
status: 1
avatar_url: https://test.jpg

这时再执行 rails test 就可以开始测试。

上面的一些 .yml 文件中,一些数据不是很规范,但只要能够保证逻辑、格式正确,能够正确使用就可以,如果在此之上进行更规范地编写也是一种可取的做法。

Rails 测试中的断言

在上述测试文件中,用到了许多 assert 的方法,如 assert_equal 判断两个变量是否相等, assert_response 判断 HTML 响应,这也是 Rails 为我们提供的一些便利,其余一些 assert 方法如下所示:

Assertion Purpose
assert( test, [msg] ) Ensures that test is true.
refute( test, [msg] ) Ensures that test is false.
assert_equal( expected, actual, [msg] ) Ensures that expected == actual is true.
refute_equal( expected, actual, [msg] ) Ensures that expected != actual is true.
assert_same( expected, actual, [msg] ) Ensures that expected.equal?(actual) is true.
refute_same( expected, actual, [msg] ) Ensures that expected.equal?(actual) is false.
assert_nil( obj, [msg] ) Ensures that obj.nil? is true.
refute_nil( obj, [msg] ) Ensures that obj.nil? is false.
assert_match( regexp, string, [msg] ) Ensures that a string matches the regular expression.
refute_match( regexp, string, [msg] ) Ensures that a string doesn't match the regular expression.
assert_in_delta( expecting, actual, [delta], [msg] ) Ensures that the numbers expected and actual are within delta of each other.
refute_in_delta( expecting, actual, [delta], [msg] ) Ensures that the numbers expected and actual are not within delta of each other.
assert_throws( symbol, [msg] ) { block } Ensures that the given block throws the symbol.
assert_raises( exception1, exception2, ... ) { block } Ensures that the given block raises one of the given exceptions.
assert_nothing_raised( exception1, exception2, ... ) { block } Ensures that the given block doesn't raise one of the given exceptions.
assert_instance_of( class, obj, [msg] ) Ensures that obj is an instance of class.
refute_instance_of( class, obj, [msg] ) Ensures that obj is not an instance of class.
assert_kind_of( class, obj, [msg] ) Ensures that obj is or descends from class.
refute_kind_of( class, obj, [msg] ) Ensures that obj is not an instance of class and is not descending from it.
assert_respond_to( obj, symbol, [msg] ) Ensures that obj responds to symbol.
refute_respond_to( obj, symbol, [msg] ) Ensures that obj does not respond to symbol.
assert_operator( obj1, operator, [obj2], [msg] ) Ensures that obj1.operator(obj2) is true.
refute_operator( obj1, operator, [obj2], [msg] ) Ensures that obj1.operator(obj2) is false.
assert_send( array, [msg] ) Ensures that executing the method listed in array[1] on the object in array[0] with the parameters of array[2 and up] is true. This one is weird eh?
flunk( [msg] ) Ensures failure. This is useful to explicitly mark a test that isn't finished yet.

除此之外还有一些更加精确的断言方法,详细可以参考有关Rails test的官方手册

A Guide to Testing Rails Applications

测试覆盖率

使用 simplecov,只需要在 Gemfile 文件中为 test 环境添加一行

gem 'simplecov', '~>0.9.0'

再执行 ./bin/bundle install 就可以安装 gem 包。

如上所说 test_helper.rb 文件中保存测试的默认设置,我们只需要在其中加入对包的引入与启动即可:

require 'simplecov'
SimpleCov.start

再次运行 rails test 时会在测试结束时显示覆盖率,并在 coverage 文件夹下生成覆盖信息 coverage/index.html,页面如下图所示:

能够看到各个文件的覆盖信息,也可以查看具体文件覆盖信息,如下图所示:

总结

Rails 无论是开发还是测试,都为我们提供了许多便利之处,但如何学会并运用这些便利,是 Rails 学习的一大难点。另外,Ruby 这门语言的更新之快,也让它难以捉摸,它的版本太多,有时最新的版本和之前的并不兼容,也造成了一定的学习困难。总而言之,Ruby 确实是一门博大精深的语言。

Ruby on Rails 单元测试的更多相关文章

  1. ruby on rails笔记

    一.新建rails项目步骤: 1.生成新项目 rails new demo cd demo vi Gemfile 末尾end前增加   gem 'execjs'   gem 'therubyracer ...

  2. Linux超快速安装Ruby on Rails

    Linux超快速安装Ruby on Rails 时间 2014-11-25 11:45:11 Flincllck Talk 原文  http://www.flincllck.com/quick-ins ...

  3. 管理不同版本ruby和rails的利器——rvm

    近年来,ruby on rails逐渐火了起来,我想各位码农早就耳闻,特别是那些做B/S项目的童鞋,早就想跃跃一试了. 笔者也是初次接触ruby on rails ,我想,对于初学者来说,最好的学习方 ...

  4. ruby on rails on windows

    这次想系统学会rails,最终目标是将redmine改造成顺手的工具,主要的手段就是开发redmine插件.虽然网上都推荐使用类Unix系统,可手头只有win7系统,就安装了. 难免会遇到这样那样的问 ...

  5. win8平台下Ruby on Rails的第一个web应用

    最近在做一个网站web前端的前期开发,老板要求用Ruby on Rails搭建部署开发环境,上网搜之,发现整个搭建流程比较坑爹,于是用了一款集成软件Bitnami Ruby Stack一键安装到我的w ...

  6. 为什么学习Ruby On Rails:

    简单总结了一下自己为什么喜欢ruby on rails: 语法简单,写代码很愉快,比较接近伪代码: 喜欢其强大的正则表达式和字符串操作. ruby中面向对象更自由,更动态: ruby给人信任,相信你了 ...

  7. 通过Ruby On Rails 框架来更好的理解MVC框架

    通过Ruby On Rails 框架来更好的理解MVC框架   1.背景    因为我在学习软件工程课程的时候,对于 MVC 框架理解不太深入,只是在理论层面上掌握,但是不知道如何在开发中使用 MVC ...

  8. Ruby on Rails框架开发学习

    学习地址:http://www.ixueyun.com/lessons/detail-lessonId-685.html 一.课程概述 软件开发在经历了面向过程编程的阶段,现在正大行其道的是敏捷开发, ...

  9. ruby on rails 2.3+的版本不再支持cgi

    ruby on rails 2.3+的版本不再支持cgi了,恶心到了,换其他框架,看了款cramp,完全没资料,完全不让人入门 操蛋的厉害,ruby果然是小众的窝里乐,放弃使用

随机推荐

  1. SpringBoot-异步定时-邮件任务

    目录 背景 异步任务 定时任务 邮件任务 背景 在我们的工作中,常常会用到异步处理任务,比如我们在网站上发送邮件, 后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功, 所以我们 ...

  2. 分享一则Linux系统邮件提示 /usr/local/lib/libprocesshider.so > /etc/ld.so.preload 的中病毒解决方法

    ​ 系统环境:CentOS Linux release 7.6.1810 (AltArch)                   CPU架构:ARM            最近发现生产服务器CPU占用 ...

  3. Vite详解

    vite 目录 一.推荐两个插件插件 Volar Vue 3 Snippets 二.vite简介 优势分析 浏览器支持 三.vite搭建vue3.x项目 1.创建项目 2.集成Vue-Router 3 ...

  4. 【转】Linux 查看端口占用情况

    Linux 查看端口占用情况可以使用 lsof 和 netstat 命令. lsof lsof(list open files)是一个列出当前系统打开文件的工具. lsof 查看端口占用语法格式: l ...

  5. shell脚本中 /dev/null 的用途

    /dev/null 是一个特殊的设备文件,它丢弃一切写入其中的数据 可以将它 视为一个黑洞, 它等效于只写文件, 写入其中的所有内容都会消失, 尝试从中读取或输出不会有任何结果,同样,/dev/nul ...

  6. [转载]让你玩转代码的编辑神器phpstorm功能详解

    转载网址:http://wwwquan.com/show-66-121-1.html phpstorm包含了webstorm的全部功能,更能够支持php代码.PhpStorm是一个轻量级且便捷的PHP ...

  7. P5404-[CTS2019]重复【KMP,dp】

    正题 题目链接:https://www.luogu.com.cn/problem/P5404 题目大意 给出一个字符串\(S\),然后求有多少个长度为\(m\)的串\(T\)满足.无限多个串\(T\) ...

  8. 小记SpringMVC与SpringBoot 的controller的返回json数据的不同

    近期由于项目的改动变更,在使用springmvc和springboot测试的时候发现一个有趣的现象 1.springmvc的controller使用@ResponseBody返回的仅仅是json格式的 ...

  9. MySQL MVCC原理深入探索

    一.MVCC的由来 二.MVCC的实际应用 RR级别场景 RC级别场景 三.MVCC的实现 3.1 多版本的数据从哪里来--Undo Log 3.1.1 插入操作对应的undo log 3.1.2 删 ...

  10. Serverless 工程实践 | Serverless 应用优化与调试秘诀

    作者|刘宇   前言:本文将以阿里云函数计算为例,提供了在线调试.本地调试等多种应用优化与调试方案. Serverless 应用调试秘诀 在应用开发过程中,或者应用开发完成,所执行结果不符合预期时,我 ...