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
注意到: 在倒數第四行中的 text_area_tag 將會用來對專案中的任務做些運算。 如果這裡使用了 form_for 所提供的 f.text_area :tasks , Rails 會嘗試讓 text_area 欄位中的值,變成 task 屬性的值。 但是 task 並不是 Project 類別的屬性,因此才需要 text_area_tag。
目前測試將送出表格,但是會以失敗收場。現階段的測試會在 project controller 裡,尋找被『送出表格』所喚醒的 create 方法。
Going with Workflow
在建立專案時,測試表格裡除了需要填寫名稱外,還需要檢視ㄧ系列的名單,並從中挑選任務。因此只有傳遞 params 的 create() 方法已經不能滿足設定的需求了,很明顯地我們需要撰寫ㄧ段 coding logic 來解決挑選任務這個『商業邏輯』。
這個需要撰寫的 coding logic,不管你將它放在哪裡,Rails 仍然會堅持有 controller 的存在,因此需要決定該如何(或者要不要),測試最後落在 controller 裡的邏輯。
我們先來探討商業邏輯這個部分,之後再回到 controller。
以此專案管理系統為例,當原有 ActiveRecord#create 中的 “pass the params hash” 已經不足以應付使用者輸入的商業邏輯時,新的商業邏輯可以放置在以下 3 個常見的位置:
- 新增的邏輯放在 Controller
- 較適合簡易的邏輯。 缺點是會增加測試的難度、不好重構程式碼、以及當需要分享邏輯時,會比較麻煩。
- 新增的邏輯放在相關的 Model
- 比放在 controller 更好,但是仍然不易於程式碼的重構,並且要熟悉 Ruby class method 的語意不是ㄧ件簡單的事。
- 另外建立不同的 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 類別就可以通過測試了
備註: 如果你得到以下錯誤訊息
NameError: expected file ...create_project.rb to define constant CreatesProject, but didn't creates_project.rb
就代表你需要把該檔案放在 app > models 資料夾內
注意到 build 方法只會創造 project object 但是不會進行儲存。
作者通常在建立 action object 的時候,會將 『initialization』、『execution』、和 『persisting the result』 分離出來。 這樣做的理由是在不需要資料庫的情形下,使用 workflow 類別來撰寫測試會加更容易及快速。
另一個作者巧思是在 initialize() 方法中帶入 Hash 型態的引數 (name: “”) ,來確保之後傳入的值是正確的。
下一步再把其餘『字串分析』的測試補上
測試裡的 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 類別上:
大致來說,新增的商業邏輯及相關測試就完成了。 不過在 creates_project_spec.rb 有許多重複的相同設定及類似編碼,在下一篇文章就會提到幾種不同的 RSpec 編碼風格來處理這樣的情形。
Comments