# 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)),以防測試欺騙我們。
圖 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 練習