Chapter 2: Test-Driven Development Basics
Back on Test
接著再繼續上一篇未完成的測試,需要讓任務本身能夠被標注『是否已經完成』。
require "rails_helper"
RSpec.describe Task do
let(:task) { Task.new }
it "does not have a new task as complete" do
expect(task).not_to be_complete
end
it "allows us to complete a task" do
task.mark_completed
expect(task).to be_complete
end
end
此測試目前都是處於在 API 的層級,並且沒有要求代表或是取代完成任務的基礎機制。 這意味著只要 API 仍然是正常運作中,你可以改變『編碼實作』而不會搞砸了測試。 過度地依附在『編碼實作』上的細節會造成在測試環境下會容易出錯,所以『行為』才是應該要被描述的,而不是特定『編碼實作』上的做法。
Prescription 4: When possible, write your tests to describe your code’s behavior, not its implementation. ― Noel Rappin ―
要通過這個測試可在 Task 類別加入以下幾個實例方法:
class Task
def initialize
@completed = false
end
def mark_completed
@completed = true
end
def complete?
@completed
end
end
再新增ㄧ個測試,確保專案有判斷狀態的能力
it "marks a project done if its tasks are done" do
project.tasks << task
task.mark_completed
expect(project).to be_done
end
在 done? 方法加上新的邏輯就會通過測試了
class Project
attr_accessor :tasks
def initialize
@tasks = []
end
def done?
tasks.all?(&:complete?)
end
end
Adding Some Math
接著我們需要讓系統計算專案內還有多少未完成的任務?、完成率是多少?,並以這些結果來預測任務完成的日期。
接下來的測試就是計算未完成的任務有多少,要寫測試之前,可先構思這個測試需要什麼? 一般來說典型的測試結構有三大部分:
- Given (What data does the test need?)
- 這個測試會需要什麼資料? => 會需要一個專案、至少一個完成跟未完成的任務。
- When (What action is taking place?)
- 什麼動作會在這個測試產生? => 計算未完成任務的行為。
- Then (What behavior do I need to specify)
- 什麼行為是需要具體說明的? => 工作計算結果
” I also like to think about what could make the happy-path test fail; that’s more helpful once you get the happy-path test in place and then start looking at it critically.“ ― Noel Rappin ―
回答完以上三個問題之後,便可以開始下一個測試了。
1 describe "estimates" do
2 let(:project) { Project.new }
3 let(:done) { Task.new(size: 2, completed: true) }
4 let(:small_not_done) { Task.new(size: 1) }
5 let(:large_not_done) { Task.new(size: 4) }
6
7 before(:each) do
8 project.tasks = [done, small_not_done, large_not_done]
9 end
10
11 it "can calculate total size" do
12 expect(project.total_size).to eq(7)
13 end
14 it "can calculate remaining size" do
15 expect(project.remaining_size).to eq(5)
16 end
17 end
由於此次測試的前置作業與先前的 describe 區塊不同,因此另外新增 describe 區塊來描述 estimates 該做什麼。 在 describe 區塊中的兩個 it block, 皆使用由 let statment 以及 before block 組合的前置作業。對 RSpec 而言,在跑每個 spec 之前,都會優先執行 before(:each) 或是 before(:example) 區塊中的程式碼。
呼應上面介紹的三大典型測試結構,通常 specs 會遵循常見的 RSpec 模式:
- Given data (測試資料) 會轉變成為一系列的 let statement
- When action (測試動作) 會轉變成為 before 區塊內的程式碼
- Then (測試各別條件) 會轉變成為一系列的 it statement
Prescription 6: Choose your test data and test-variable names to make it easy to diagnose failures when they happen. Meaningful names and data that doesn’t overlap are helpful. ― Noel Rappin ―
這次的測試會在第3行: Task.new(size: 2, completed: true) 爆出錯誤,因為 Task 類別並不是 Rails 的 ActiveRecord,所以預設的引數不會是 hash 型態。 找出問題後,就手動改寫成以下程式碼吧!
class Task
attr_accessor :size, :completed
def initialize(options = {})
@completed = options[:completed]
@size = options[:size]
end
def mark_completed
@completed = true
end
def complete?
@completed
end
end
接著在15行,因為 expect(project.remaining_size).to eq(5) 又爆出錯誤 所以需要新增 remaining_size() 方法給 Project 類別
class Project
attr_accessor :tasks
def initialize
@tasks = []
end
def done?
tasks.all?(&:complete?)
end
def total_size
tasks.sum(&:size)
end
def remaining_size
tasks.reject(&:complete?).sum(&:size)
end
end
通過測試後,又再次回到 TDD Cycle 的最後一個步驟: 重構程式碼
仔細檢查 Project class 後,可以發現有兩個方法都使用了未完成任務的名單:
- done? 方法: 詢問『未完成的任務名單』是否空白,如果空白的話就應當回傳 true
- remining_size 方法: 計算『未完成的任務名單』的長度
因此可以新增 incomplete_tasks() 方法,再從這裡來簡化
class Project
attr_accessor :tasks
def initialize
@tasks = []
end
def incomplete_tasks
tasks.reject(&:complete?)
end
def done?
incomplete_tasks.empty?
end
def total_size
tasks.sum(&:size)
end
def remaining_size
incomplete_tasks.sum(&:size)
end
end
雖然程式碼重構之後,提高了閱讀性,不過程式碼卻沒有精簡多少。但之後如果對任務完成的定義有改變,就只需改變在 incomplete_tasks() 方法內做修改即可。
Comments