在线版的内容可能落后于电子书,如果想及时获得更新,请购买电子书

第 3 章 基本静态的页面

从本章开始,我们将开发一个专业级演示应用,本书后续章节会一直开发这个应用。最终完成的应用包含用户、微博功能,以及完整的登录和用户身份验证系统,不过我们先从一个看似功能有限的话题出发——创建静态页面。这看似简单的一件事却是一个很好的锻炼,极具意义,对这个初建的应用而言也是个很好的开端。

虽然 Rails 被设计出来是为了开发以数据库为后台的动态网站,不过它也能胜任使用纯 HTML 创建的静态页面。其实,使用 Rails 创建静态页面有一个好处:添加少量动态内容十分容易。这一章就教你怎么做。在这个过程中,我们会一窥自动化测试(automated testing)的面目。自动化测试可以让我们相信自己编写的代码是正确的。而且,编写一个好的测试组件还可以让我们信心十足地重构代码,修改实现过程但不影响功能。

3.1 创建演示应用

第 2 章一样,我们将先创建一个新 Rails 项目,名为 sample_app,如代码清单 3.1 所示。[1]

代码清单 3.1:创建一个新演示应用
$ cd ~/environment
$ rails _5.1.6_ new sample_app
$ cd sample_app/

(与 2.1 节一样,如果使用云端 IDE,可以在同一个工作空间中创建这个应用,没必要再新建一个工作空间。)

注意:为了便于参考,本书实现的完整演示应用可以在 Bitbucket 中查看。

类似 2.1 节,接下来我们要用文本编辑器打开并编辑 Gemfile 文件,写入应用所需的 gem。代码清单 3.2代码清单 1.5代码清单 2.1 一样,不过 test 组中的 gem 有所不同,这些是高级测试设置(3.6 节)和集成测试(5.3.4 节)所需的。注意,如果现在你想安装这个应用使用的所有 gem,要写入代码清单 13.72 中的内容。

代码清单 3.2:这个演示应用的 Gemfile 文件
source 'https://rubygems.org'

gem 'rails',        '5.1.6'
gem 'puma',         '3.9.1'
gem 'sass-rails',   '5.0.6'
gem 'uglifier',     '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks',   '5.0.1'
gem 'jbuilder',     '2.7.0'

group :development, :test do
  gem 'sqlite3', '1.3.13'
  gem 'byebug',  '9.0.6', platform: :mri
end

group :development do
  gem 'web-console',           '3.5.1'
  gem 'listen',                '3.0.8'
  gem 'spring',                '2.0.2'
  gem 'spring-watcher-listen', '2.0.1'
end

group :test do
  gem 'rails-controller-testing', '1.0.2'
  gem 'minitest-reporters',       '1.1.14'
  gem 'guard',                    '2.13.0'
  gem 'guard-minitest',           '2.4.4'
end

group :production do
  gem 'pg', '0.20.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

与前两章一样,我们要执行 bundle install 命令安装并引入 Gemfile 文件中指定的 gem,而且要指定 --without production 选项,[2]不安装生产环境使用的 gem:

$ bundle install --without production

运行上述命令后不会在开发环境中安装 PostgreSQL 所需的 pg gem,在生产环境和测试环境中我们使用 SQLite。Heroku 不建议在开发环境和生产环境中使用不同的数据库,但是对这个演示应用来说,这两种数据库没什么差别,而且在本地安装、配置 SQLite 比 PostgreSQL 容易得多。[3]如果你之前安装了某个 gem(例如 Rails 本身)的其他版本,与 Gemfile 中指定的版本号不同,最好再执行 bundle update 命令更新 gem,确保安装的版本和指定的一致:

$ bundle update

最后,我们要初始化 Git 仓库:

$ git init
$ git add -A
$ git commit -m "Initialize repository"

与第一个应用一样,我建议你更新一下 README 文件,更好地描述这个应用。我们先把这个文件中的内容删掉,然后换成代码清单 3.3 中的 Markdown 内容。注意,README 文件中说明了如何安装这个应用。(直到第 6 章才会执行 rails db:migrate 命令,不过现在写上也无妨。)

代码清单 3.3:修改这个演示应用的 README 文件
README.md
# Ruby on Rails Tutorial sample application

This is the sample application for
[*Ruby on Rails Tutorial:
Learn Web Development with Rails*](http://www.railstutorial.org/)
by [Michael Hartl](http://www.michaelhartl.com/).

## License

All source code in the [Ruby on Rails Tutorial](http://railstutorial.org/)
is available jointly under the MIT License and the Beerware License. See
[LICENSE.md](LICENSE.md) for details.

## Getting started

To get started with the app, clone the repo and then install the needed gems:

```
$ bundle install --without production
```

Next, migrate the database:

```
$ rails db:migrate
```

Finally, run the test suite to verify that everything is working correctly:

```
$ rails test
```

If the test suite passes, you'll be ready to run the app in a local server:

```
$ rails server
```

For more information, see the
[*Ruby on Rails Tutorial* book](http://www.railstutorial.org/book).

然后,提交改动:

$ git commit -am "Improve the README"

你可能还记得,在 1.4.4 节,我们使用 git commit -a -m "Message" 命令,指定了表示“全部变化”的旗标 -a 和提交信息的旗标 -m。如上述命令所示,我们可以把两个旗标合在一起,写成 git commit -am "Message"

既然本书后续内容会一直使用这个演示应用,那么最好在 Bitbucket 中新建一个仓库,把这个应用推送上去:

$ git remote add origin git@bitbucket.org:<username>/sample_app.git
$ git push -u origin --all # 首次推送这个应用

为了避免以后遇到焦头烂额的问题,在这个早期阶段也可以把应用部署到 Heroku 中。参照第 1 章第 2 章,我建议像代码清单 3.4代码清单 3.5 那样做,创建一个显示“hello, world!”的首页。(之所以这样做是因为 Rails 的默认页面往往无法在 Heroku 中显示,很难判断部署成功还是失败。)

代码清单 3.4:在 Application 控制器中添加 hello 动作
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def hello
    render html: "hello, world!"
  end
end
代码清单 3.5:设置根路由
config/routes.rb
Rails.application.routes.draw do
  root 'application#hello'
end

然后提交改动,推送到 Bitbucket 和 Heroku 中:

$ git commit -am "Add hello"
$ git push
$ heroku create
$ git push heroku master

1.5 节一样,你可能会看到一些警告消息,现在暂且不管,7.5 节会解决。除了 Heroku 为应用分配的地址之外,看到的页面应该和图 1.23 一样。

在阅读本书的过程中,我建议你定期推送和部署,这样不仅能在远程仓库中备份,还能尽早发现在生产环境中可能出现的问题。如果遇到与 Heroku 有关的问题,可以查看生产环境中的日志,试着找出问题所在:

$ heroku logs

注意,如果你决定把真实的应用部署到 Heroku 中,一定要按照 7.5 节介绍的方法配置生产环境的 Web 服务器。

练习

  1. 确认 Bitbucket 把代码清单 3.3 中的 Markdown 渲染成了 HTML。

  2. 访问生产环境中应用的根路由,确认成功部署到 Heroku 中了。

3.2 静态页面

前一节的准备工作做好之后,我们可以开始开发这个演示应用了。本节,我们要向开发动态页面迈出第一步:创建一些 Rails 动作和视图,但只包含静态 HTML。[4]Rails 动作放在控制器中(MVC 中的 C,参见 1.3.3 节),用于组织相关的功能。第 2 章已经简要介绍了控制器,全面熟悉 REST 架构之后(从第 6 章开始),你会更深入地理解控制器。回想一下 1.3 节介绍的 Rails 项目目录结构(图 1.7),会对我们有所帮助。这一节主要在 app/controllersapp/views 两个目录中工作。

1.4.4 节说过,使用 Git 时最好在单独的主题分支中完成工作,不要直接使用主分支。如果你使用 Git 做版本控制,现在应该执行下述命令,切换到一个主题分支,然后再创建静态页面:

$ git checkout -b static-pages

3.2.1 生成静态页面

下面我们要使用第 2 章用来生成脚手架的 generate 命令生成一个控制器。既然这个控制器用来处理静态页面,那就把它命名为 StaticPages 吧。可以看出,控制器的名字使用驼峰式命名法。我们计划创建“首页”、“帮助”页面和“关于”页面,对应的动作名分别为 homehelpaboutgenerate 命令可以接收一个可选的参数列表,指定要创建的动作。我们将在命令行中指定 homehelp 动作,故意不指定 about 动作,3.3 节再介绍怎么添加。生成 StaticPages 控制器的命令如代码清单 3.6 所示。

代码清单 3.6:生成 StaticPages 控制器
$ rails generate controller StaticPages home help
      create  app/controllers/static_pages_controller.rb
       route  get 'static_pages/help'
       route  get 'static_pages/home'
      invoke  erb
      create    app/views/static_pages
      create    app/views/static_pages/home.html.erb
      create    app/views/static_pages/help.html.erb
      invoke  test_unit
      create    test/controllers/static_pages_controller_test.rb
      invoke  helper
      create    app/helpers/static_pages_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/static_pages.coffee
      invoke    scss
      create      app/assets/stylesheets/static_pages.scss

顺便说一下,rails generate 可以简写成 rails g。除此之外,Rails 还提供了几个命令的简写形式,参见表 3.1。为了明确表述,本书会一直使用命令的完整形式,但在实际使用中,大多数 Rails 开发者或多或少都会使用表 3.1 中的简写形式。[5]

表 3.1:一些 Rails 命令的简写形式
完整形式 简写形式

$ rails server

$ rails s

$ rails console

$ rails c

$ rails generate

$ rails g

$ rails test

$ rails t

$ bundle install

$ bundle

在继续之前,如果你使用 Git,最好把 StaticPages 控制器对应的文件推送到远程仓库:

$ git add -A
$ git commit -m "Add a Static Pages controller"
$ git push -u origin static-pages

最后一个命令的意思是,把 static-pages 主题分支推送到 Bitbucket。以后再推送时,可以省略后面的参数,简写成:

$ git push

在现实的开发过程中,我一般都会先提交再推送,但是为了行文简洁,从这往后我们会省略提交这一步。

注意,在代码清单 3.6 中,我们传入的控制器名使用驼峰式(因为像骆驼的双峰一样),创建的控制器文件名则是蛇底式。所以,传入“StaticPages”得到的文件是 static_pages_controller.rb。这只是一种约定。其实在命令行中也可以使用蛇底式:

$ rails generate controller static_pages ...

这个命令也会生成名为 static_pages_controller.rb 的控制器文件。因为 Ruby 的类名使用驼峰式(4.4 节),所以提到控制器时我会使用驼峰式,不过这是我的个人选择。(因为 Ruby 文件名一般使用蛇底式,所以 Rails 生成器使用 underscore 方法把驼峰式转换成蛇底式。)

顺便说一下,如果在生成代码时出错了,知道如何撤销操作就很有用了。旁注 3.1 中介绍了一些在 Rails 中撤销操作的方法。

代码清单 3.6 中生成 StaticPages 控制器的命令会自动修改路由文件(config/routes.rb)。我们在 1.3.4 节已经简略介绍过这个文件,它的作用是实现 URL 和网页之间的对应关系(图 2.11)。路由文件在 config 目录中。Rails 在这个目录中存放应用的配置文件(图 3.1)。

因为生成控制器时我们指定了 homehelp 动作,所以路由文件中已经添加了相应的规则,如代码清单 3.7 所示。

代码清单 3.7StaticPages 控制器中 homehelp 动作的路由
config/routes.rb
Rails.application.routes.draw do
  get  'static_pages/home'
  get  'static_pages/help'
  root 'application#hello'
end
config directory 4th edition
图 3.1:演示应用 config 目录中的内容

如下的规则

get 'static_pages/home'

把发给 /static_pages/home 的请求映射到 StaticPages 控制器的 home 动作上。另外,get 表明这个路由响应的是 GET 请求。GET 是 HTTP(Hypertext Transfer Protocol,超文本传输协议)支持的基本请求方法之一(旁注 3.2)。这里,当我们在 StaticPages 控制器中生成 home 动作时,就自动在 /static_pages/home 地址上获得了一个页面。若想查看这个页面,按照 1.3.2 节中的方法,启动 Rails 开发服务器:

$ rails server

然后访问 /static_pages/home,如图 3.2 所示。

raw home view 3rd edition
图 3.2:简陋的首页(/static_pages/home)

要想弄明白这个页面是怎么来的,我们先在文本编辑器中看一下 StaticPages 控制器文件。你应该会看到类似代码清单 3.8 所示的内容。你可能注意到了,与第 2 章中的 UsersMicroposts 控制器不同,StaticPages 控制器没使用标准的 REST 动作。这对静态页面来说是很常见的,毕竟 REST 架构不能解决所有问题。

代码清单 3.8代码清单 3.6 生成的 StaticPages 控制器
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
  end

  def help
  end
end

从上面代码中的 class 关键字可以看出,static_pages_controller.rb 文件中定义了一个类,名为 StaticPagesController。类是一种组织函数(也叫方法)的便利方式,例如 homehelp 动作就是方法,使用 def 关键字定义。2.3.4 节说过,尖括号 < 表示 StaticPagesController 继承自 ApplicationController 类;稍后你会看到,这意味着我们定义的页面拥有了 Rails 提供的大量功能。(我们会在 4.4 节更详细地介绍类和继承。)

现在,StaticPages 控制器中的两个方法都是空的:

def home
end

def help
end

如果是普通的 Ruby 代码,这两个方法什么也做不了。不过在 Rails 中就不一样了。StaticPagesController 是一个 Ruby 类,但是因为它继承自 ApplicationController,其中的方法对 Rails 来说就有了特殊意义:访问 /static_pages/home 时,Rails 会在 StaticPages 控制器中寻找 home 动作,然后执行该动作,再渲染相应的视图(MVC 中的 V,参见 1.3.3 节)。这里,home 动作是空的,所以访问 /static_pages/home 后只会渲染视图。那么,视图是什么样子,怎么才能找到它呢?

如果你再看一下代码清单 3.6 的输出,或许能猜到动作和视图之间的对应关系:home 动作对应的视图是 home.html.erb3.4 节会告诉你 .erb 是什么意思。看到 .html 你或许就不奇怪了,这个文件基本上就是 HTML,如代码清单 3.9 所示。

代码清单 3.9:为“首页”生成的视图
app/views/static_pages/home.html.erb
<h1>StaticPages#home</h1>
<p>Find me in app/views/static_pages/home.html.erb</p>

help 动作的视图类似,如代码清单 3.10 所示。

代码清单 3.10:为“帮助”页面生成的视图
app/views/static_pages/help.html.erb
<h1>StaticPages#help</h1>
<p>Find me in app/views/static_pages/help.html.erb</p>

这两个视图都只是占位用的,它们的内容中都有一个一级标题(h1 标签)和一个显示视图文件完整路径的段落(p 标签)。

练习
  1. 生成含有 barbaz 两个动作的 Foo 控制器。

  2. 使用旁注 3.1 中介绍的技术删除 Foo 控制器及相关的动作。

3.2.2 修改静态页面中的内容

我们会在 3.4 节添加一些简单的动态内容。现在,这些静态内容的存在是为了强调一件很重要的事:Rails 的视图可以只包含静态的 HTML。所以我们甚至无需了解 Rails 就可以修改“首页”和“帮助”页面的内容,如代码清单 3.11代码清单 3.12 所示。

代码清单 3.11:修改“首页”的 HTML
app/views/static_pages/home.html.erb
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>
代码清单 3.12:修改“帮助”页面的 HTML
app/views/static_pages/help.html.erb
<h1>Help</h1>
<p>
  Get help on the Ruby on Rails Tutorial at the
  <a href="http://www.railstutorial.org/help">Rails Tutorial help page</a>.
  To get help on this sample app, see the
  <a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
  book</a>.
</p>

修改之后,这两个页面显示的内容如图 3.3图 3.4 所示。

custom home page
图 3.3:修改后的“首页”
custom help page 4th edition
图 3.4:修改后的“帮助”页面

3.3 开始测试

我们创建并修改了“首页”和“帮助”页面的内容,下面要添加“关于”页面。做这样的改动时,最好编写自动化测试,确认实现的方法是否正确。对本书开发的应用来说,我们编写的测试组件有两个作用:其一,作为一种安全防护措施;其二,作为源码的文档。虽然要编写额外的代码,但是如果方法得当,测试能协助我们快速开发,因为有了测试之后,查找问题所用的时间会变少。不过,我们要善于编写测试才行,所以要尽早开始练习。

几乎每个 Rails 开发者都认同测试是好习惯,但具体的作法多种多样。最近有一场针对测试驱动开发(Test-Driven Development,简称 TDD)的争论[6],十分热闹。TDD 是一种测试技术,程序员要先编写失败的测试,然后再编写应用代码,让测试通过。本书采用一种轻量级、符合直觉的测试方案,只在适当的时候才使用 TDD,而不严格遵守 TDD 理念(旁注 3.3)。

我们主要编写的测试类型是控制器测试(本节开始编写)、模型测试(第 6 章开始编写)和集成测试(第 7 章开始编写)。集成测试的作用特别大,它能模拟用户在浏览器中与应用交互的过程,最终会成为我们的主要关注对象,不过控制器测试更容易上手。

3.3.1 第一个测试

现在我们要在这个应用中添加一个“关于”页面。我们将看到,这个测试很短,所以按照旁注 3.3中的指导方针,我们先编写测试,然后使用失败的测试驱动我们编写应用代码。

着手测试是件具有挑战的事情,要求对 Rails 和 Ruby 都有深入的了解。这么早就编写测试可能有点儿让你害怕。不过,Rails 已经为我们解决了最难的部分,因为执行 rails generate controller 命令时(代码清单 3.6)自动生成了一个测试文件,我们可以从这个文件入手:

$ ls test/controllers/
static_pages_controller_test.rb

我们来看一下这个文件的内容,如代码清单 3.13 所示。

代码清单 3.13:默认为 StaticPages 控制器生成的测试 GREEN
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end
end

现在无需理解详细的句法,不过可以看出,其中有两个测试,对应我们在命令行中传入的两个动作(代码清单 3.6)。各个测试先访问 URL,然后(通过断言)确认得到的是成功响应。其中,get 表示测试期望这两个页面是普通的网页,可以通过 GET 请求访问(旁注 3.2);:success 响应(表示 200 OK)是对 HTTP 响应码的抽象表示。也就是说,下面这个测试的意思是:为了测试首页,向 StaticPages 控制器中 home 动作对应的 URL 发起 GET 请求,确认得到的是表示成功的响应码。

test "should get home" do
  get static_pages_home_url
  assert_response :success
end

测试循环的第一步是运行测试组件,确认测试现在可以通过。我们要执行下述命令:

代码清单 3.14GREEN
$ rails test
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

与预期一样,一开始测试组件可以通过(GREEN)。(如果没按照 3.6.1 节的说明添加 MiniTest 报告程序,不会看到绿色。不过,即使看不到真正的绿色,我们也经常这样表述。)在某些系统中,测试要花相当长的时间才能启动,这是因为(1)要启动 Spring 服务器,预载部分 Rails 环境,不过只有首次启动时会受此影响;(2)启动 Ruby 要花点儿时间。(第二点可以使用 3.6.2 节推荐的 Guard 改善。)

3.3.2 遇红

我们在旁注 3.3 中说过,测试驱动开发流程是先编写一个失败测试,然后编写应用代码让测试通过,最后再根据需要重构代码。因为很多测试工具都使用红色表示失败的测试,使用绿色表示通过的测试,所以这个流程有时也叫“遇红-变绿-重构”循环。这一节我们先完成这个循环的第一步,编写一个失败测试,即“遇红”。3.3.3 节“变绿”,3.4.3 节“重构”。[7]

首先,我们要为“关于”页面编写一个失败测试。参照代码清单 3.13,你或许能猜到该怎么写,如代码清单 3.15 所示。

代码清单 3.15:“关于”页面的测试 RED
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
  end
end

如高亮显示的那几行所示,为“关于”页面编写的测试与首页和“帮助”页面的测试类似,只不过把“home”或“help”换成了“about”。

与预期一样,这个测试现在失败:

代码清单 3.16RED
$ rails test
3 tests, 2 assertions, 0 failures, 1 errors, 0 skips

3.3.3 变绿

现在有了一个失败测试(RED),我们要在这个失败测试的错误消息指示下,让测试通过(GREEN),也就是要实现一个可以访问的“关于”页面。

先看一下这个失败测试给出的错误消息:

代码清单 3.17RED
$ rails test
NameError: undefined local variable or method `static_pages_about_url'

这个错误消息说,未定义获取“关于”页面地址的 Rails 代码,其实就是提示我们要在路由文件中添加一个规则。参照代码清单 3.7,我们可以编写如代码清单 3.18 所示的路由。

代码清单 3.18:添加 about 路由 RED
config/routes.rb
Rails.application.routes.draw do
  get  'static_pages/home'
  get  'static_pages/help'
  get  'static_pages/about'
  root 'application#hello'
end

这段代码中高亮的那行告诉 Rails,把发给 /static_pages/about 页面的 GET 请求交给 StaticPages 控制器的 about 动作处理。这条规则会自动创建一个辅助方法:

static_pages_about_url

再次运行测试组件,仍然无法通过,不过错误消息变了:

代码清单 3.19RED
$ rails test
AbstractController::ActionNotFound:
The action 'about' could not be found for StaticPagesController

这个错误消息的意思是,StaticPages 控制器缺少 about 动作。我们可以参照代码清单 3.8 中的 homehelp 编写这个动作,如代码清单 3.20 所示。

代码清单 3.20:在 StaticPages 控制器中添加 about 动作 RED
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
  end

  def help
  end

  def about
  end
end

现在测试依旧失败,不过测试消息又变了:

$ rails test
ActionController::UnknownFormat: StaticPagesController#about is missing
a template for this request format and variant.

这表明没有模板。在 Rails 中,模板就是视图。3.2.1 节说过,home 动作对应的视图是 home.html.erb,保存在 app/views/static_pages 目录中。所以,我们要在这个目录中新建一个文件,而且要命名为 about.html.erb

在不同的系统中新建文件有不同的方法,不过大多数情况下都可以在想要新建文件的目录中点击鼠标右键,然后在弹出的菜单中选择“新建文件”。我们也可以使用文本编辑器的“文件”菜单,新建文件后再选择保存的位置。除此之外,还可以使用我最喜欢的 Unix touch 命令,如下所示:

$ touch app/views/static_pages/about.html.erb

如《Learn Enough Command Line to Be Dangerous》所讲,touch 的作用是更新文件或文件夹的修改时间戳,但有个副作用:如果文件不存在,它会新建一个空文件。(如果使用云端 IDE,或许要刷新文件树,参见 1.3.1 节。这也体现了“技术是复杂的”。)

在正确的目录中创建 about.html.erb 文件之后,写入代码清单 3.21 中的内容。

代码清单 3.21:“关于”页面的内容 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>

现在执行 rails test 命令,会看到测试能通过了:

代码清单 3.22GREEN
$ rails test
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

当然,我们还可以在浏览器中查看这个页面(图 3.5),以防测试欺骗我们。

about us 3rd edition
图 3.5:新添加的“关于”页面(/static_pages/about)

3.3.4 重构

现在测试已经变绿,我们可以自信地尽情重构了。开发应用时,代码经常会“变味”(意思是代码会变得丑陋、啰嗦,有大量重复)。电脑不会在意,但是人类会,所以经常重构,把代码变简洁一些是很重要的事情。我们的演示应用现在还很小,没什么可重构的,不过代码无时无刻不在变味,所以 3.4.3 节就将开始重构。

3.4 有点动态内容的页面

我们已经为几个静态页面创建了动作和视图,现在要稍微添加一些动态内容,根据所在的页面不同而变化:我们要让标题根据页面的内容变化。改变标题到底算不算真正动态还有争议,但是这么做能为第 7 章实现的真正动态内容打下基础。

我们的计划是修改首页、“帮助”页面和“关于”页面,让每页显示的标题都不一样。为此,我们要在页面的视图中使用 <title> 标签。大多数浏览器都会在浏览器窗口的顶部显示标题中的内容,而且标题对搜索引擎优化(Search-Engine Optimization,简称 SEO)也有好处。我们要使用完整的“遇红-变绿-重构”循环:先为页面的标题编写一些简单的测试(遇红),然后分别在三个页面中添加标题(变绿),最后使用布局文件去除重复内容(重构)。本节结束时,三个静态页面的标题都会变成“<页面的名字> | Ruby on Rails Tutorial Sample App”这种形式(表 3.2)。

rails new 命令创建了一个布局文件,不过现在最好不用。我们重命名这个文件:

$ mv app/views/layouts/application.html.erb layout_file   # 临时改动

在真实的应用中你不需要这么做,不过没有这个文件能让你更好地理解它的作用。

表 3.2:这个演示应用中基本上是静态内容的页面
页面 URL 基本标题 变动部分

首页

/static_pages/home

"Ruby on Rails Tutorial Sample App"

"Home"

帮助

/static_pages/help

"Ruby on Rails Tutorial Sample App"

"Help"

关于

/static_pages/about

"Ruby on Rails Tutorial Sample App"

"About"

3.4.1 测试标题(遇红)

添加标题之前,我们要知道网页的一般结构,如代码清单 3.23 所示。(《Learn Enough HTML to Be Dangerous》对这个话题有深入说明。)

代码清单 3.23:网页一般的 HTML 结构
<!DOCTYPE html>
<html>
  <head>
    <title>Greeting</title>
  </head>
  <body>
    <p>Hello, world!</p>
  </body>
</html>

这段代码的最顶部是文档类型声明(document type declaration,简称 doctype),作用是告诉浏览器使用哪个 HTML 版本(这里使用的是 HTML5)。[8]随后是 head 部分,里面有一个 title 标签,其内容是“Greeting”。然后是 body 部分,里面有一个 p 标签(段落),其内容是“Hello, world!”。(缩进是可选的,HTML 不会特别对待空白,制表符和空格都会被忽略,但缩进可以让文档结构更清晰。)

我们要使用 assert_select 方法分别为表 3.2 中的每个标题编写简单的测试,然后合并到代码清单 3.15 中。assert_select 方法的作用是检查有没有指定的 HTML 标签。这种方法有时也叫“选择符”(selector),因此才为这个方法取这么一个名称。[9]

assert_select "title", "Home | Ruby on Rails Tutorial Sample App"

这行代码的作用是检查有没有 <title> 标签,以及其中的内容是不是“Home | Ruby on Rails Tutorial Sample App”字符串。把这样的代码分别放到三个页面的测试中,得到的结果如代码清单 3.24 所示。

代码清单 3.24:加入标题测试后的 StaticPages 控制器测试 RED
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

写好测试之后,应该确认一下现在测试组件是失败的(RED):

代码清单 3.25RED
$ rails test
3 tests, 6 assertions, 3 failures, 0 errors, 0 skips

3.4.2 添加页面标题(变绿)

现在,我们要为每个页面添加标题,让前一节的测试通过。参照代码清单 3.23 中的 HTML 结构,把代码清单 3.11 中的首页内容换成代码清单 3.26 中的内容。

代码清单 3.26:具有完整 HTML 结构的首页 RED
app/views/static_pages/home.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Home | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>
    <p>
      This is the home page for the
      <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
      sample application.
    </p>
  </body>
</html>

修改之后的首页如图 3.6 所示。[10]

home view full html 4th ed
图 3.6:添加标题后的首页

使用类似的方式修改“帮助”页面和“关于”页面,得到的代码如代码清单 3.27代码清单 3.28 所示。

代码清单 3.27:具有完整 HTML 结构的“帮助”页面 RED
app/views/static_pages/help.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Help | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Help</h1>
    <p>
      Get help on the Ruby on Rails Tutorial at the
      <a href="http://www.railstutorial.org/help">Rails Tutorial help
      page</a>.
      To get help on this sample app, see the
      <a href="http://www.railstutorial.org/book"><em>Ruby on Rails
      Tutorial</em> book</a>.
    </p>
  </body>
</html>
代码清单 3.28:具有完整 HTML 结构的“关于”页面 GREEN
app/views/static_pages/about.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>About | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <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>
  </body>
</html>

现在,测试组件能通过了:

代码清单 3.29GREEN
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
练习

从本节开始,我们将修改应用代码,而且这些改动不会体现在以后的代码清单中。这么做是为了让没有做练习的读者能读懂正文,因为解答练习所需的代码与正文有差异。这也体现了“技术是复杂的”。

  1. 你可能注意到了,StaticPages 控制器的测试(代码清单 3.24)中有些重复,每个标题测试中都有“Ruby on Rails Tutorial Sample App”。我们可以使用特殊的函数 setup 去除重复。这个函数在每个测试运行之前执行。请你确认代码清单 3.30 中的测试仍能通过。(代码清单 3.30 中使用了一个实例变量,2.2.2 节简单介绍过,4.4.5 节会进一步说明。这段代码还使用了字符串插值操作,4.2.2 节会做进一步说明。)

代码清单 3.30:使用一个基标题的 StaticPages 控制器测试 GREEN
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | #{@base_title}"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | #{@base_title}"
  end
end

3.4.3 布局和嵌入式 Ruby(重构)

到目前为止,本节已经做了很多事情,我们使用 Rails 控制器和动作生成了三个可用的页面,不过这些页面中的内容都是纯静态的 HTML,没有体现出 Rails 的强大之处。而且,代码中有大量重复:

  • 页面的标题几乎(但不完全)是一模一样的;

  • 每个标题中都有“Ruby on Rails Tutorial Sample App”;

  • 整个 HTML 结构在每个页面都重复地出现了。

重复的代码违反了很重要的“不要自我重复”(Don’t Repeat Yourself,简称 DRY)原则。本节要遵照 DRY 原则,去掉重复的代码。最后,我们要运行前一节编写的测试,确认显示的标题仍然正确。

不过,去除重复的第一步却是要增加一些代码,让页面的标题看起来是一样的。这样我们就能更容易地去掉重复的代码了。

在这个过程中,我们要在视图中使用嵌入式 Ruby(Embedded Ruby)。既然首页、“帮助”页面和“关于”页面的标题中有一个变动的部分,那我们就使用 Rails 提供的一个特别的函数 provide,在每个页面中设定不同的标题。通过把 home.html.erb 视图中标题的“Home”换成代码清单 3.31 所示的代码,我们可以看一下这个函数的作用。

代码清单 3.31:标题中使用了嵌入式 Ruby 代码的首页视图 GREEN
app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>
    <p>
      This is the home page for the
      <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
      sample application.
    </p>
  </body>
</html>

在这段代码中我们第一次使用了嵌入式 Ruby(简称 ERb)。(现在你应该知道为什么 HTML 视图文件的扩展名是 .html.erb 了。)ERb 是为网页添加动态内容使用的主要模板系统。[11]下面的代码

<% provide(:title, 'Home') %>

通过 <% …​ %> 调用 Rails 提供的 provide 函数,把字符串 "Home" 赋给 :title[12]然后,在标题中,我们使用类似的符号 <%= …​ %>,通过 Ruby 的 yield 函数把标题插入模板中:[13]

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

(这两种嵌入式 Ruby 代码的区别在于,<% …​ %>执行其中的代码;<%= …​ %> 除了执行其中的代码,还会把执行的结果插入模板中。)最终得到的页面跟以前一样,不过,现在标题中变动的部分通过 ERb 动态生成。

我们可以运行前一节编写的测试确认一下。现在,测试还能通过:

代码清单 3.32GREEN
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

然后,按照相同的方式修改“帮助”页面(代码清单 3.33)和“关于”页面(代码清单 3.34)。

代码清单 3.33:标题中使用了嵌入式 Ruby 代码的“帮助”页面视图 GREEN
app/views/static_pages/help.html.erb
<% provide(:title, "Help") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Help</h1>
    <p>
      Get help on the Ruby on Rails Tutorial at the
      <a href="http://www.railstutorial.org/help">Rails Tutorial help
      section</a>.
      To get help on this sample app, see the
      <a href="http://www.railstutorial.org/book"><em>Ruby on Rails
      Tutorial</em> book</a>.
    </p>
  </body>
</html>
代码清单 3.34:标题中使用了嵌入式 Ruby 代码的“关于”页面视图 GREEN
app/views/static_pages/about.html.erb
<% provide(:title, "About") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <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>
  </body>
</html>

至此,我们把页面标题中的变动部分都换成了 ERb。现在,各个页面的内容类似下面这样:

<% provide(:title, "The Title") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    Contents
  </body>
</html>

也就是说,所有页面的结构都是一样的,包括 title 标签中的内容,只有 body 标签中的内容有些差别。

为了提取出共用的结构,Rails 提供了一个特别的布局文件,名为 application.html.erb。我们在 3.4 节的开头重命名了这个文件,现在改回来:

$ mv layout_file app/views/layouts/application.html.erb

若想使用这个布局,我们要把默认的标题换成前面几段代码中使用的嵌入式 Ruby:

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

修改后得到的布局文件如代码清单 3.35 所示。

代码清单 3.35:这个演示应用的网站布局 GREEN
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                                              'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

注意,其中有一行比较特殊:

<%= yield %>

这行代码的作用是,把每个页面的内容插入布局中。没必要了解它的具体实现过程,我们只需知道,在布局中使用这行代码后,访问 /static_pages/home 时会把 home.html.erb 中的内容转换成 HTML,然后插入 <%= yield %> 所在的位置。

还要注意,默认的 Rails 布局文件中有下面这几行代码:

<%= csrf_meta_tags %>
<%= stylesheet_link_tag ... %>
<%= javascript_include_tag "application", ... %>

这几行代码的作用是,引入应用的样式表和 JavaScript 文件(Asset Pipeline 的一部分,5.2.1 节会介绍);Rails 提供的 csrf_meta_tags 方法,作用是避免跨站请求伪造(Cross-Site Request Forgery,简称 CSRF,一种恶意网络攻击)。

现在,代码清单 3.31代码清单 3.33代码清单 3.34 的内容还是和布局文件中的 HTML 结构类似,所以我们要把完整的结构删除,只保留需要的内容。清理后的视图如代码清单 3.36代码清单 3.37代码清单 3.38 所示。

代码清单 3.36:去除完整的 HTML 结构后的首页 GREEN
app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
  sample application.
</p>
代码清单 3.37:去除完整的 HTML 结构后的“帮助”页面 GREEN
app/views/static_pages/help.html.erb
<% provide(:title, "Help") %>
<h1>Help</h1>
<p>
  Get help on the Ruby on Rails Tutorial at the
  <a href="http://www.railstutorial.org/help">Rails Tutorial help section</a>.
  To get help on this sample app, see the
  <a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
  book</a>.
</p>
代码清单 3.38:去除完整的 HTML 结构后的“关于”页面 GREEN
app/views/static_pages/about.html.erb
<% provide(:title, "About") %>
<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>

修改这几个视图后,首页、“帮助”页面和“关于”页面显示的内容还和之前一样,但是没有多少重复内容了。

经验告诉我们,即便是十分简单的重构,也容易出错,所以才要认真编写测试组件。有了测试,我们就无需手动检查每个页面,看有没有错误。初期阶段手动检查还不算难,但是当应用不断变大之后,情况就不同了。我们只需验证测试组件是否还能通过即可:

代码清单 3.39GREEN
$ rails test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

测试不能证明代码完全正确,但至少能提高正确的可能性,而且还提供了安全防护措施,能避免以后出问题。

练习
  1. 为这个演示应用添加一个“联系”页面。[14]参照代码清单 3.15,先编写一个测试,检查页面的标题是否为 “Contact | Ruby on Rails Tutorial Sample App”,从而确定 /static_pages/contact 对应的页面是否存在。参照 3.3.3 节添加“关于”页面的步骤,把代码清单 3.40 中的内容写入“联系”页面的视图,让测试通过。

代码清单 3.40:“联系”页面的内容
app/views/static_pages/contact.html.erb
<% provide(:title, "Contact") %>
<h1>Contact</h1>
<p>
  Contact the Ruby on Rails Tutorial about the sample app at the
  <a href="http://www.railstutorial.org/contact">contact page</a>.
</p>

3.4.4 设置根路由

我们修改了网站中的页面,也顺利开始编写测试了,在继续之前,我们要设置应用的根路由。与 1.3.4 节2.2.2 节的做法一样,我们要修改 routes.rb 文件,把根路径 / 指向我们选择的页面。这里我们要指向前面创建的首页。(我还建议把 3.1 节添加的 hello 动作从 Application 控制器中删除。)如代码清单 3.41 所示,我们要把 root 规则由

root 'application#hello'

改成

root 'static_pages#home'

这样对 / 的请求就交给 StaticPages 控制器的 home 动作处理了。修改路由后,首页如图 3.7 所示。

home root route
图 3.7:在根路由上显示的首页
代码清单 3.41:把根路由指向“首页”
config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get  'static_pages/home'
  get  'static_pages/help'
  get  'static_pages/about'
end
练习
  1. 添加代码清单 3.41 中的根路由后,会得到一个名为 root_url 的辅助方法(与 static_pages_home_url 类似)。把代码清单 3.42 中的 FILL_IN 改成真正的代码,测试根路由。

  2. 因为事先编写好了代码清单 3.41 中的那些代码,前一题的测试已经可以通过。但是,我们很难确信测试是正确的。修改代码清单 3.41 中的代码,把根路由注释掉(如代码清单 3.43 所示,4.2.1 节会进一步介绍注释),先“遇红”。然后,去掉注释(还原成代码清单 3.41 那样),确认测试可以通过。

代码清单 3.42:测试根路由 GREEN
test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get root" do
    get FILL_IN
    assert_response FILL_IN
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
  end
end
代码清单 3.43:注释掉根路由,让测试失败 RED
config/routes.rb
Rails.application.routes.draw do
#   root 'static_pages#home'
  get  'static_pages/home'
  get  'static_pages/help'
  get  'static_pages/about'
end

3.5 小结

总的来说,本章几乎没做什么:我们从静态页面开始,最后得到的几乎还是静态内容的页面。不过从表面上看,我们使用了 Rails 中的控制器、动作和视图,现在已经可以向网站中添加任何动态内容了。本书的后续章节会告诉你怎么添加。

在继续之前,我们花一点时间把改动提交到主题分支,然后将其合并到主分支中。在 3.2 节,我们为静态页面的开发工作创建了一个新分支,在开发的过程中如果你还没有提交,那么先来做一次提交吧,因为我们已经完成了一些工作:

$ git add -A
$ git commit -m "Finish static pages"

然后,使用 1.4.4 节介绍的方法,把改动合并到主分支中:[15]

$ git checkout master
$ git merge static-pages

每次完成一些工作后,最好把代码推送到远程仓库中(如果你按照 1.4.3 节中的步骤做了,远程仓库在 Bitbucket 中):

$ git push

我还建议你把这个应用部署到 Heroku 中:

$ rails test
$ git push heroku

在部署之前先运行测试组件是个好习惯。

3.5.1 本章所学

  • 我们第三次介绍了从零开始创建一个新 Rails 应用的完整过程,包括安装所需的 gem、把应用推送到远程仓库,以及部署到生产环境中;

  • 执行 rails generate controller ControllerName <optional action names> 命令会生成一个新控制器;

  • config/routes.rb 文件中定义了新路由;

  • Rails 的视图中可以包含静态 HTML 或嵌入式 Ruby(ERb);

  • 测试组件能驱动我们开发新功能,给我们重构的自信,还能捕获回归;

  • 测试驱动开发使用“遇红-变绿-重构”循环;

  • Rails 的布局定义应用中页面共用的模板,可以去除重复。

3.6 高级测试技术

这一节选读,介绍本书配套视频中使用的测试设置。包含两方面内容:增强版通过和失败报告程序(3.6.1 节);一个自动测试运行程序,检测到文件有变化后自动运行相应的测试(3.6.2 节)。这一节使用的代码相对高级,放在这里只是为了查阅方便,现在并不期望你能理解。

这一节应该在主分支中修改:

$ git checkout master

3.6.1 MiniTest 报告程序

为了让 Rails 应用的测试适时显示红色和绿色,我建议你在测试辅助文件中加入代码清单 3.44 中的内容,[16]充分利用代码清单 3.2 中的 minitest-reporters gem。

代码清单 3.44:配置测试,显示红色和绿色
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require "minitest/reporters"
Minitest::Reporters.use!

class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

修改后,在云端 IDE 中显示的效果如图 3.8 所示。

red to green
图 3.8:在云端 IDE 中测试由红变绿

3.6.2 使用 Guard 自动测试

使用 rails test 命令有一点很烦人,总是要切换到命令行然后手动运行测试。为了避免这种不便,我们可以使用 Guard 自动运行测试。Guard 会监视文件系统的变动,假如你修改了 static_pages_controller_test.rb 文件,那么 Guard 只会运行这个文件中的测试。而且,我们还可以配置 Guard,让它在 home.html.erb 文件被修改后,也自动运行 static_pages_controller_test.rb 文件中的测试。

代码清单 3.2 中的 Gemfile 已经包含了 guard gem,所以我们只需初始化即可:

$ bundle exec guard init
Writing new Guardfile to /home/ec2-user/environment/sample_app/Guardfile
00:51:32 - INFO - minitest guard added to Guardfile, feel free to edit it

然后,编辑生成的 Guardfile 文件,让 Guard 在集成测试和视图发生变化后运行正确的测试,如代码清单 3.45 所示。为了尽量提高灵活性,我建议使用这里的 Guardfile:railstutorial.org/guardfile。

在云端 IDE 中还有一步要做:安装 tmux,让 Guard 发送通知。安装方法如下:

$ sudo yum install -y tmux    # 只需在云端 IDE 中执行
代码清单 3.45:修改后的 Guardfile 文件
# Defines the matching rules for Guard.
guard :minitest, spring: "bin/rails test", all_on_start: false do
  watch(%r{^test/(.*)/?(.*)_test\.rb$})
  watch('test/test_helper.rb') { 'test' }
  watch('config/routes.rb')    { integration_tests }
  watch(%r{^app/models/(.*?)\.rb$}) do |matches|
    "test/models/#{matches[1]}_test.rb"
  end
  watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|
    resource_tests(matches[1])
  end
  watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
    ["test/controllers/#{matches[1]}_controller_test.rb"] +
    integration_tests(matches[1])
  end
  watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
    integration_tests(matches[1])
  end
  watch('app/views/layouts/application.html.erb') do
    'test/integration/site_layout_test.rb'
  end
  watch('app/helpers/sessions_helper.rb') do
    integration_tests << 'test/helpers/sessions_helper_test.rb'
  end
  watch('app/controllers/sessions_controller.rb') do
    ['test/controllers/sessions_controller_test.rb',
     'test/integration/users_login_test.rb']
  end
  watch('app/controllers/account_activations_controller.rb') do
    'test/integration/users_signup_test.rb'
  end
  watch(%r{app/views/users/*}) do
    resource_tests('users') +
    ['test/integration/microposts_interface_test.rb']
  end
end

# Returns the integration tests corresponding to the given resource.
def integration_tests(resource = :all)
  if resource == :all
    Dir["test/integration/*"]
  else
    Dir["test/integration/#{resource}_*.rb"]
  end
end

# Returns the controller tests corresponding to the given resource.
def controller_test(resource)
  "test/controllers/#{resource}_controller_test.rb"
end

# Returns all tests for the given resource.
def resource_tests(resource)
  integration_tests(resource) << controller_test(resource)
end

下面这行代码会让 Guard 使用 Rails 提供的 Spring 服务器,从而减少加载时间,而且启动时不运行整个测试组件。

guard :minitest, spring: "bin/rails test", all_on_start: false do
file navigator gear icon
图 3.9:文件浏览器中的齿轮图标(不太好找)
show hidden files
图 3.10:在文件浏览器中显示隐藏文件
gitignore
图 3.11:通常隐藏的 .gitignore 文件出现了

使用 Guard 时,为了避免 Spring 和 Git 发生冲突,应该把 spring/ 目录添加到 .gitignore 文件中,让 Git 忽略它。在云端 IDE 中要这么做:

  1. 点击文件浏览器右上角的齿轮图标,如图 3.9 所示;

  2. 选择“Show hidden files”(显示隐藏文件),让 .gitignore 文件出现在应用的根目录中,如图 3.10 所示;

  3. 双击打开 .gitignore 文件(图 3.11),写入代码清单 3.46 中的内容。

代码清单 3.46:把 Spring 添加到 .gitignore 文件中
# See https://help.github.com/articles/ignoring-files for more about
# ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
#   git config --global core.excludesfile '~/.gitignore_global'

# Ignore bundler config.
/.bundle

# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore Byebug command history file.
.byebug_history

# Ignore Spring files.
/spring/*.pid

写作本书时,Spring 服务器还有点儿怪异,有时 Spring 进程会不断拖慢测试的运行速度。如果你发现测试变得异常缓慢,最好查看系统进程(旁注 3.4),如果需要,把 Spring 进程杀死。

配置好 Guard 之后,应该打开一个新终端窗口(与 1.3.2 节启动 Rails 服务器的做法一样),在其中执行下述命令:

$ bundle exec guard

代码清单 3.45 中的规则针对本书做了优化,例如,修改控制器后会自动运行集成测试。如果想运行所有测试,在 guard> 提示符中按回车键。(有时会看到一个错误,说连接 Spring 服务器失败。再次按回车键就能解决这个问题。)

若想退出 Guard,按 Ctrl-D 键。如果想为 Guard 添加其他匹配器,参阅代码清单 3.45Guard 的自述文件维基

继续之前,应该添加改动,做次提交:

$ git add -A
$ git commit -m "Complete advanced setup"
  1. 如果使用云端 IDE,可以使用“Goto Anything”命令,输入部分文件名就能方便地在文件系统中找到所需的文件。现在三个应用都放在同一个工作空间中,只输入文件名效果可能不很理想。例如,如果查找名为“Gemfile”的文件,会出现六个结果,因为每个应用中都有能匹配查找条件的两个文件:GemfileGemfile.lock。因此,你可以把前两个应用删除,方法是:进入 environment 文件夹,执行 rm -rf hello_app/ toy_app/ 命令(参见表 1.1)。只要你之前把这两个应用推送到 Bitbucket 中了,以后再恢复都很容易。
  2. 注意,这个选项会被 Bundler 记住,下次只需运行 bundle install 即可。
  3. 现在时机还不成熟,但我建议你以后一定要学会如何在开发环境中安装、配置 PostgreSQL。届时,可以在谷歌中搜索“install configure postgresql <your system>”,以及“rails postgresql setup”。在云端 IDE 中,“<your system>”是 Linux。
  4. 这里讲的静态页面创建方法可能是最简单的,但不是唯一的,你应该根据需求使用合适的方法。如果要创建大量静态页面,使用静态页面控制器太麻烦,不过这个演示应用只需要几个静态页面。如果需要创建大量静态页面,可以使用 high_voltage gem。
  5. 其实,很多 Rails 开发者还会为 rails 命令创建别名(参阅《Learn Enough Text Editor to Be Dangerous》),简化成 r。这样,使用简洁的 r s 命令就能启动 Rails 服务器。
  6. 详情参见 Rails 创始人 David Heinemeier Hansson 写的一篇文章:TDD is dead. Long live testing
  7. 默认情况下,执行 rails test 命令后,如果测试失败会显示红色,但测试通过不会显示绿色。若想得到由红变绿的过程,参照 3.6.1 节的说明。
  8. HTML 一直在变化,显式声明一个 doctype 可以确保未来的浏览器还可以正确解析页面。<!DOCTYPE html> 这种极为简单的格式是最新的 HTML 标准 HTML5 的一个特色。
  9. Rails 指南中说明测试的文章列出了常用的 MiniTest 断言。
  10. 书中大多数截图都使用 Google Chrome,但是这张截图使用的是 Safari,因为 Chrome 无法显示完整的标题。(写作本书时,Safari 必须有多个标签页才会显示页面标题,因此图 3.6 中有两个标签页。)此外请注意,图 3.6 中的 URL 其实是 /static_pages/home,而 Safari 只显示了基 URL,即 Cloud9 为我分配的开发服务器地址。
  11. 还有一种受欢迎的模板系统是 Haml(不是“HAML”),我个人很喜欢用,不过在这样的初级教程中使用不太合适。
  12. 经验丰富的 Rails 开发者可能觉得这里应该使用 content_for,可是它在 Asset Pipeline 中有点问题。provide 函数是替代方案。
  13. 如果你学过 Ruby,可能会猜测 Rails 是把内容“拽入”区块中的,这么想也对。不过使用 Rails 开发应用不必知道这一点。
  14. 这个练习会在 5.3.1 节完成。
  15. 如果报错说合并会覆盖 Spring 进程 ID(PID)文件,在命令行中执行 rm -f *.pid 命令,把那个文件删掉。
  16. 代码清单 3.44 既使用了单引号形式字符串,也使用了双引号形式字符串,因为 rails new 命令生成的文件使用单引号字符串,而 MiniTest 报告程序的文档中使用双引号。在 Ruby 代码中混用两种形式的字符串很常见,详情参见 4.2.2 节