# 3.3 开始测试
我们创建并修改了“首页”和“帮助”页面的内容,下面要添加“关于”页面。做这样的改动时,最好编写自动化测试确认实现的方法正确。对本书开发的应用来说,我们编写的测试组件有两个作用:其一,是一种安全防护措施;其二,作为源码的文档。虽然要编写额外的代码,但是如果方法得当,测试能协助我们快速开发,因为有了测试查找问题所用的时间会变少。不过,我们要善于编写测试才行,所以要尽早开始练习。
几乎每个 Rails 开发者都认同测试是好习惯,但具体的作法多种多样。最近有一场针对“测试驱动开发”(Test-Driven Development,简称 TDD)的辩论[[4](#fn-4)],十分热闹。TDD 是一种测试技术,程序员要先编写失败的测试,然后再编写应用的代码,让测试通过。本书采用一种轻量级,符合直觉的测试方案,只在适当的时候才使用 TDD,而不严格遵守 TDD 理念([旁注 3.3](#aside-when-to-test))。
##### 旁注 3.3:什么时候测试
判断何时以及如何测试之前,最好弄明白为什么要测试。在我看来,编写自动化测试主要有三个好处:
1. 测试能避免“回归”(regression),即由于某些原因之前能用的功能不能用了;
2. 有测试,重构(改变实现方式,但功能不变)时更有自信;
3. 测试是应用代码的客户,因此可以协助我们设计,以及决定如何与系统的其他组件交互。
以上三个好处都不要求先编写测试,但在很多情况下,TDD 仍有它的价值。何时以及如何测试,部分取决于你编写测试的熟练程度。很多开发者发现,熟练之后,他们更倾向于先编写测试。除此之外,还取决于测试较之应用代码有多难,你对想实现的功能有多深的认识,以及未来在什么情况下这个功能会遭到破坏。
现在,最好有一些指导方针,告诉我们什么时候应该先写测试(以及什么时候完全不用测试)。根据我自己的经验,给出一些建议:
* 和应用代码相比,如果测试代码特别简短,倾向于先编写测试;
* 如果对想实现的功能不是特别清楚,倾向于先编写应用代码,然后再编写测试,改进实现的方式;
* 安全是头等大事,保险起见,要为安全相关的功能先编写测试;
* 只要发现一个问题,就编写一个测试重现这种问题,以避免回归,然后再编写应用代码修正问题;
* 尽量不为以后可能修改的代码(例如 HTML 结构的细节)编写测试;
* 重构之前要编写测试,集中测试容易出错的代码。
在实际的开发中,根据上述方针,我们一般先编写控制器和模型测试,然后再编写集成测试(测试模型、视图和控制器结合在一起时的表现)。如果应用代码很容易出错,或者经常会变动(视图就是这样),我们就完全不测试。
我们主要编写的测试类型是控制器测试(本节开始编写),模型测试([第 6 章](chapter6.html#modeling-users)开始编写)和集成测试([第 7 章](chapter7.html#sign-up)开始编写)。集成测试的作用特别大,它能模拟用户在浏览器中和应用交互的过程,最终会成为我们的主要关注对象,不过控制器测试更容易上手。
## 3.3.1 第一个测试
现在我们要在这个应用中添加一个“关于”页面。我们会看到,这个测试很简短,所以按照[旁注 3.3](#aside-when-to-test)中的指导方针,我们要先编写测试。然后使用失败的测试驱动我们编写应用代码。
着手测试是件具有挑战的事情,要求对 Rails 和 Ruby 都有深入的了解。这么早就编写测试可能有点儿吓人。不过,Rails 已经为我们解决了最难的部分,因为执行 `rails generate controller` 命令时([代码清单 3.4](#listing-generating-pages))自动生成了一个测试文件,我们可以从这个文件入手:
```
$ ls test/controllers/
static_pages_controller_test.rb
```
我们看一下这个文件的内容,如[代码清单 3.11](#listing-default-controller-test) 所示。
##### 代码清单 3.11:为静态页面控制器生成的测试 GREEN
test/controllers/static_pages_controller_test.rb
```
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
end
test "should get help" do
get :help
assert_response :success
end
end
```
现在无需理解详细的句法,不过可以看出,其中有两个测试,对应我们在命令行中传入的两个动作([代码清单 3.4](#listing-generating-pages))。在每个测试中,先访问动作,然后确认(通过“断言”)得到正确的响应。其中,`get` 表示测试期望这两个页面是普通的网页,可以通过 `GET` 请求访问([旁注 3.2](#aside-get-etc));`:success` 响应是对 HTTP [响应码](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)的抽象表示(在这里表示 [200 OK](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success))。也就是说,下面这个测试
```
test "should get home" do
get :home
assert_response :success
end
```
它的意思是:我们要测试首页,那么就向 `home` 动作发起一个 `GET` 请求,确认得到的是表示成功的响应码。
下面我们要运行测试组件,确认测试现在可以通过。方法是,按照下面的方式运行 `rake` 任务([旁注 2.1](chapter2.html#aside-rake)):[[5](#fn-5)]
##### 代码清单 3.12:**GREEN**
```
$ bundle exec rake test
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
```
按照需求,一开始测试组件可以通过(**GREEN**)。(如果没按照 [3.7.1 节](#minitest-reporters)的说明添加 MiniTest 报告程序,不会看到绿色。)顺便说一下,测试要花点时间启动,因为(1)要启动 Spring 服务器预加载部分 Rails 环境,不过这一步只在首次启动时执行;(2)启动 Ruby 也要花点儿时间。(第二点可以使用 [3.7.3 节](#automated-tests-with-guard)推荐的 Guard 改善。)
## 3.3.2 遇红
我们在[旁注 3.3](#aside-when-to-test)中说过,TDD 流程是,先编写一个失败测试,然后编写应用代码让测试通过,最后再按需重构代码。因为很多测试工具都使用红色表示失败的测试,使用绿色表示通过的测试,所以这个流程有时也叫“遇红-变绿-重构”循环。这一节我们先完成这个循环的第一步,编写一个失败测试,“遇红”。然后在 [3.3.3 节](#green)变绿,[3.4.3 节](#layouts-and-embedded-ruby)重构。[[6](#fn-6)]
首先,我们要为“关于”页面编写一个失败测试。参照[代码清单 3.11](#listing-default-controller-test),你或许能猜到应该怎么写,如[代码清单 3.13](#listing-about-test) 所示。
##### 代码清单 3.13:“关于”页面的测试 RED
test/controllers/static_pages_controller_test.rb
```
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
end
test "should get help" do
get :help
assert_response :success
end
test "should get about" do get :about assert_response :success end end
```
如高亮显示的那几行所示,为“关于”页面编写的测试与首页和“帮助”页面的测试一样,只不过把“home”或“help”换成了“about”。
这个测试现在失败:
##### 代码清单 3.14:**RED**
```
$ bundle exec rake test
3 tests, 2 assertions, 0 failures, 1 errors, 0 skips
```
## 3.3.3 变绿
现在有了一个失败测试(**RED**),我们要在这个失败测试的错误消息指示下,让测试通过(**GREEN**),也就是要实现一个可以访问的“关于”页面。
我们先看一下这个失败测试给出的错误消息:[[7](#fn-7)]
##### 代码清单 3.15:**RED**
```
$ bundle exec rake test
ActionController::UrlGenerationError:
No route matches {:action=>"about", :controller=>"static_pages"}
```
这个错误消息说,没有找到需要的动作和控制器组合,其实就是提示我们要在路由文件中添加一个规则。参照[代码清单 3.5](#listing-pages-routes),我们可以编写如[代码清单 3.16](#listing-about-route) 所示的路由。
##### 代码清单 3.16:添加 `about` 路由 RED
config/routes.rb
```
Rails.application.routes.draw do
get 'static_pages/home'
get 'static_pages/help'
get 'static_pages/about' .
.
.
end
```
这段代码中高亮显示的那行告诉 Rails,把发给 /static_pages/about 页面的 `GET` 请求交给静态页面控制器中的 `about` 动作处理。
然后再运行测试组件,仍然无法通过,不过错误消息变了:
##### 代码清单 3.17:**RED**
```
$ bundle exec rake test
AbstractController::ActionNotFound:
The action 'about' could not be found for StaticPagesController
```
这个错误消息的意思是,静态页面控制器中缺少 `about` 动作。我们可以参照[代码清单 3.6](#listing-static-pages-controller) 编写这个动作,如[代码清单 3.18](#listing-adding-the-about-page) 所示。
##### 代码清单 3.18:在静态页面控制器中添加 `about` 动作 RED
app/controllers/static_pages_controller.rb
```
class StaticPagesController < ApplicationController
def home
end
def help
end
def about end end
```
现在测试依旧失败,不过测试消息又变了:
```
$ bundle exec rake test ActionView::MissingTemplate: Missing template static_pages/about
```
这表示没有模板。在 Rails 中,模板就是视图。[3.2.1 节](#generated-static-pages)说过,`home` 动作对应的视图是 `home.html.erb`,保存在 `app/views/static_pages` 文件夹中。所以,我们要在这个文件夹中新建一个文件,并且要命名为 `about.html.erb`。
在不同的系统中新建文件有不同的方法,不过大多数情况下都可以在想要新建文件的文件夹中点击鼠标右键,然后在弹出的菜单中选择“新建文件”。或者,可以使用文本编辑器的“文件”菜单,新建文件后再选择保存的位置。除此之外,还可以使用我最喜欢的 [Unix `touch` 命令](http://en.wikipedia.org/wiki/Touch_(Unix)),用法如下:
```
$ touch app/views/static_pages/about.html.erb
```
`touch` 的主要作用是更新文件或文件夹的修改时间戳,别无其他效果,但有个副作用,如果文件不存在,就会新建一个。(如果使用云端 IDE,或许要刷新文件树,参见 [1.3.1 节](chapter1.html#bundler)。)
在正确的文件夹中创建 `about.html.erb` 文件之后,要在其中写入[代码清单 3.19](#listing-custom-about-page) 中的内容。
##### 代码清单 3.19:“关于”页面的内容 GREEN
app/views/static_pages/about.html.erb
```
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
```
现在,运行 `rake test`,会看到测试通过了:
##### 代码清单 3.20:**GREEN**
```
$ bundle exec rake test
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
```
当然,我们还可以在浏览器中查看这个页面([图 3.5](#fig-about-us)),以防测试欺骗我们。
![about us 3rd edition](https://box.kancloud.cn/2016-05-11_5732bcf653839.png)图 3.5:新添加的“关于”页面([/static_pages/about](http://localhost:3000/static_pages/about))
## 3.3.4 重构
现在测试已经变绿了,我们可以自信地尽情重构了。开发应用时,代码经常会“变味”(意思是代码会变得丑陋、啰嗦,有大量的重复)。电脑不会在意,但是人类会,所以经常重构把代码变简洁一些是很重要的事情。我们的演示应用现在还很小,没什么可重构的,不过代码无时无刻不在变味,所以 [3.4.3 节](#layouts-and-embedded-ruby)就要开始重构。
- Ruby on Rails 教程
- 致中国读者
- 序
- 致谢
- 作者译者简介
- 版权和代码授权协议
- 第 1 章 从零开始,完成一次部署
- 1.1 简介
- 1.2 搭建环境
- 1.3 第一个应用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小结
- 1.7 练习
- 第 2 章 玩具应用
- 2.1 规划应用
- 2.2 用户资源
- 2.3 微博资源
- 2.4 小结
- 2.5 练习
- 第 3 章 基本静态的页面
- 3.1 创建演示应用
- 3.2 静态页面
- 3.3 开始测试
- 3.4 有点动态内容的页面
- 3.5 小结
- 3.6 练习
- 3.7 高级测试技术
- 第 4 章 Rails 背后的 Ruby
- 4.1 导言
- 4.2 字符串和方法
- 4.3 其他数据类型
- 4.4 Ruby 类
- 4.5 小结
- 4.6 练习
- 第 5 章 完善布局
- 5.1 添加一些结构
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的链接
- 5.4 用户注册:第一步
- 5.5 小结
- 5.6 练习
- 第 6 章 用户模型
- 6.1 用户模型
- 6.2 用户数据验证
- 6.3 添加安全密码
- 6.4 小结
- 6.5 练习
- 第 7 章 注册
- 7.1 显示用户的信息
- 7.2 注册表单
- 7.3 注册失败
- 7.4 注册成功
- 7.5 专业部署方案
- 7.6 小结
- 7.7 练习
- 第 8 章 登录和退出
- 8.1 会话
- 8.2 登录
- 8.3 退出
- 8.4 记住我
- 8.5 小结
- 8.6 练习
- 第 9 章 更新,显示和删除用户
- 9.1 更新用户
- 9.2 权限系统
- 9.3 列出所有用户
- 9.4 删除用户
- 9.5 小结
- 9.6 练习
- 第 10 章 账户激活和密码重设
- 10.1 账户激活
- 10.2 密码重设
- 10.3 在生产环境中发送邮件
- 10.4 小结
- 10.5 练习
- 10.6 证明超时失效的比较算式
- 第 11 章 用户的微博
- 11.1 微博模型
- 11.2 显示微博
- 11.3 微博相关的操作
- 11.4 微博中的图片
- 11.5 小结
- 11.6 练习
- 第 12 章 关注用户
- 12.1 “关系”模型
- 12.2 关注用户的网页界面
- 12.3 动态流
- 12.4 小结
- 12.5 练习