Chapter 3: Test-Driven Rails

Making the Test Pass

end-to-end test
― Rails 5 Test Prescriptions (Rails 5.1, RSpec 3.7) ―

讓 RSpec 去跑上面的 end-to-end test 後,會得到第一個錯誤訊息:

NameError: undefined local variable or method `new_project_path'

會得到這個錯誤訊息是因為在 Chapter 2 建立的 Project 類別並不是標準的 ActiveRecord resources,也沒有對應的路徑,因此 RSpec 沒有辦法找到 new_project_path 路徑。

解決之道其一是使用 rails generate 指令如下:

rails g resource project name:string due_date:date

rails g resource task project:references title:string size:integer completed_at:datetime

因為已經使用 ActiveRecord 了,所以要移除原本在 project.rb 的 initialize 方法,保留其餘的方法如下圖:

Project Class
― Rails 5 Test Prescriptions ―

然後在 task.rb 修改如下圖:

Project Class
― Rails 5 Test Prescriptions ―

在終端機輸入

rails db:migrate

完成之後,除了本章節加入的 end-to-end test 外,上ㄧ章節所撰寫的 spec 應該就都可以順利通過了。

Days Are Action-Packed

雖然已經新增了 new_project_path 路徑,不過測試又爆出另一個錯誤訊息:

1) adding a project allows a user to create a project with tasks
     Failure/Error: visit new_project_path

     AbstractController::ActionNotFound:
     The action 'new' could not be found for ProjectsController

很明顯的,錯誤訊息表示 Project controller 還缺了ㄧ個 new 方法:

class ProjectsController < ApplicationController
  def new
    @project = Project.new
  end
end

再次跑 RSpec 後會觸發了新的錯誤訊息,因為 RSpec 會預期在 /app/views/projects 裡找到 new.html.erb。立馬補上該檔案後,得到以下 Capybara 錯誤訊息:

1) adding a project allows a user to create a project with tasks
     Failure/Error: fill_in "Name", with: "Project Runway"

     Capybara::ElementNotFound:
       Unable to find field "Name"

Capybara 在表格中會從 DOM ID、表格名稱、或是相關的標籤尋找符合測試要求的欄位。但是 new.html.erb 還是空白的狀態,所以要增加三個表格元件來回應錯誤訊息:(1)text_field for the name (2)multiline text area for the tasks (3)submit button

Project Class
― Rails 5 Test Prescriptions ―

目前測試將送出表格,但是會以失敗收場。現階段的測試會在 project controller 裡,尋找被『送出表格』所喚醒的 create 方法。

Going with Workflow

在建立專案時,測試表格裡除了需要填寫名稱外,還需要檢視ㄧ系列的名單,並從中挑選任務。因此只有傳遞 params 的 create() 方法已經不能滿足設定的需求了,很明顯地我們需要撰寫ㄧ段 coding logic 來解決挑選任務這個『商業邏輯』。

這個需要撰寫的 coding logic,不管你將它放在哪裡,Rails 仍然會堅持有 controller 的存在,因此需要決定該如何,甚至是要不要,測試最後落在 controller 裡的任何邏輯。

我們先來探討商業邏輯這個部分,之後再回到 controller。以書中範例來說,當原有在 ActiveRecord#create 中的 “pass the params hash” 已經不夠時,新的商業邏輯可以放置在以下 3 個常見的位置:

  1. 新增的邏輯放在 controller
    • 缺點是會增加測試的難度、不好重構程式碼、以及當需要分享邏輯時,會比較麻煩。
  2. 新增的邏輯放在 associated model
    • 比放在 controller 更好,但是仍然不易於程式碼的重構因為要熟知 Ruby 類別方法的語意是痛苦的。
  3. 另外建立不同的 class 來封裝邏輯和工作流程
    • 最易於測試及處理商業邏輯改變後所增加的難度。

Placing business logic outside Rails classes makes that logic easier to test and manage. ― Rails 5 Test Prescriptions ―

接著我們選擇第三選項:建立ㄧ個邏輯類別(logic class),命名為 workflow class。 其他常見的邏輯類別名稱包括:action、service、context、use case、concern 和 factory。

Project Class
― Rails 5 Test Prescriptions ―

這是簡單明瞭的ㄧ個測試。因為我們選擇不將『程式邏輯』放置在 controller 裡面,你甚至不需要撰寫華麗的程式碼。作者之所以會把此類別取名為 CreatesProject 是因為作者不喜歡以名詞來幫 action class 命名。 其他可替代 CreatesProject 的名稱可以是: CreateProject、ProjectCreator 或是 ProjectFactory。

建立 CreatesProject 類別就可以通過測試了

Project Class
― Rails 5 Test Prescriptions ―

另ㄧ個值得注意的是 build 方法只會創造 project object 但是不會進行儲存。 當建立 action object 的時候,可以試著將 『initialization』、『execution』、和 『persisting the result』 分離出來。這樣做的理由是在不需要資料庫的情形下,使用 workflow 類別來撰寫測試會加更容易。

下一步再把其餘『字串分析』的測試補上

Project Class
Project Class
― Rails 5 Test Prescriptions ―

測試裡的 specs 是循序漸進地在進步,因為每個 spec 都是根據前一個 spec 所無法完成的前提下而產生的。值得注意是有在 specs 中有使用到 has_attributes 的 matcher。 這個 matcher 允許你用一行的程式碼就可以明確指出相同物件所擁有的多種屬性。

expect(tasks.first).to have_attributes(title: "Start Things", size: 1)

注意:在測試多個任務的 spec 裡有使用 RSpec ”matches“ matcher 來取得 data structure 並一個接一個地進行比對。

expect(tasks).to match(
    [an_object_having_attributes(title: "Start Things", size: 3),

    an_object_having_attributes(title: "End Things", size: 2)])

而這個 “an_object_having_attributes” matcher 在測試中是同等於

expect(tasks[0]).to have_attributes(title: "Start Things", size: 3)
expect(tasks[1]).to have_attributes(title: "End Things", size: 2)

測試 “save” 的行為是很重要的,因為當所有的物件都是已儲存的狀態, Rails associations 有時候會運行得更好。我們知道 Rails 是用 ID 來追蹤物件之間的關聯,而只有當物件是已儲存的時候,Rails 才會給予物件 ID。

還有ㄧ個小技巧是可用 “save!” 方法來替代 save,因為當物件是 invalid 時,“save!” 會立刻丟出 exception 讓你知道測試有問題。 及早發現才能及早治療的意思!!

再次回到 CreatesProject 類別上:

Project Class
― Rails 5 Test Prescriptions ―

終於將『字串轉換』及『任務規模的字串專換』分開來為獨立的方法。