Chapter 3: Test-Driven Rails
Making the Test Pass

讓 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 方法,保留其餘的方法如下圖:

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

在終端機輸入
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 controller 裡,尋找被『送出表格』所喚醒的 create 方法。
Going with Workflow
在建立專案時,測試表格裡除了需要填寫名稱外,還需要檢視ㄧ系列的名單,並從中挑選任務。因此只有傳遞 params 的 create() 方法已經不能滿足設定的需求了,很明顯地我們需要撰寫ㄧ段 coding logic 來解決挑選任務這個『商業邏輯』。
這個需要撰寫的 coding logic,不管你將它放在哪裡,Rails 仍然會堅持有 controller 的存在,因此需要決定該如何,甚至是要不要,測試最後落在 controller 裡的任何邏輯。
我們先來探討商業邏輯這個部分,之後再回到 controller。以書中範例來說,當原有在 ActiveRecord#create 中的 “pass the params hash” 已經不夠時,新的商業邏輯可以放置在以下 3 個常見的位置:
- 新增的邏輯放在 controller
- 缺點是會增加測試的難度、不好重構程式碼、以及當需要分享邏輯時,會比較麻煩。
- 新增的邏輯放在 associated model
- 比放在 controller 更好,但是仍然不易於程式碼的重構因為要熟知 Ruby 類別方法的語意是痛苦的。
- 另外建立不同的 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。

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

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


測試裡的 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 類別上:

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