Chapter 2: Test-Driven Development Basics
Running the Test
What Really Happens – Internally?
When you run spec with no argument, RSpec loads every file in the spec directory. The following things happen:
-
Each file in the spec directory is loaded.
-
Each RSpec file typically requires the rails helper.rb file.
-
By default the rails helper.rb file set up transactional fixtures.
-
Each top-level call to RSpec.describe creates an internal RSpec object called an example group.
-
Each top-level example group runs.
Running an example group involves running each example that it contains, and that involves a few steps:
- Run all before(:example) setup blocks.
- Run the example, which is the block argument to it.
- Run all after(:example) teardown blocks.
- Roll back or delete the fixtures as described earlier.
- After all examples in the file have run, run any after(:all) blocks.
The diagram below show the flow
Making the Test Pass
-
The purist way
Do the simplest thing that could possible work.
-
The practical way
Write the code you know you need to eventually write, effectively skipping steps that seem too small to be valuable.
-
The teaching way
This method is somewhere between the other two and lets me best explain how and why test-driven development works without getting bogged down in details or skipping too many steps.
之後作者會進階式的講解:如何從 Rspec 給的錯誤訊息撰寫出正確的編碼,以及構思下一個需要的測試為何。
以下是第一個需要通過的測試:
# project_spec.rb
1 require "rails_helper"
2
3 RSpec.describe Project do
4 it "considers a project with no tasks to be done" do
5 project = Project.new
6 expect(project.done?).to be_truthy
7 end
8
9 end
- 如果直接去跑 rspec 的話,會得到 uninitialzied constant:Project 錯誤訊息,但新增 Project class 就可以解決。
- 但是再跑 rspect,又會得到 undefined method ‘done?’ 錯誤訊息。既然找不到 done? 方法,就在 Project class 建立 done? 方法。
- 這個 done? 方法必須回傳 true ,否則就會得到 nil 值。完成後就成功地通過第一個 rspec 測試,也代表著進入到程式碼重構 (refactoring) 步驟。
class Project
def done?
true
end
end
The Second Test
我們剛剛讓 done? 方法永遠只會回傳 true,但如果專案裡的還任務沒有完成,就應該要回傳 false 才合理。 因此有了以下測試:
it "knows that a project with an incomplete task is not done" do
project = Project.new
task = Task.new
project.tasks << task
expect(project.done?).to be_falsy
end
- 跑完 rspec 的話,會得第一個錯誤訊息 Task constant is missing ,新增 Task class 就可以解決。
- 接著第二個錯誤訊息會爆出 project.tasks並不存在,解決方法就是讓 Project 類別裡加入 tasks 屬性 attr_accessor :tasks。
- 另外讓每一個 Project 的實例物件都預設能夠接受 task 的陣列,便可以在 done? 方法內,以 empty? 查詢個別 project 裡的陣列是否還有 task 存在。
class Project
attr_accessor :tasks
def initialize
@tasks = []
end
def done?
tasks.empty?
end
end
在上一個章節,我們學到了優良的 TDD 會有以下過程:
- 建立測試
- 確認在預定設定下,測試會失敗
- 撰寫最簡單就能夠通過測試的程式碼
- 通過測試後,重構並優化程式碼
目前為止,我們成功了完成前三項工作。現在就來到了最後一項:重構並優化程式碼
Let and Expectations
Let statement使用 RSpec 的 let 方法可以將重複的變數做一個整合。也就是說在相同的 describe 區塊裡,可以讓每一個 spec 使用 let 所定義的變數,而不需要『 定義變數為實例變數 』或是使用 『 before(:example) 』 來做前置作業。
在 describe 的區塊裡,每一個 let 方法都可接受ㄧ個 symbol 引數 和 block。
let(:project) { Project.new }
當第一次呼叫 let 方法內的 symbol 時,會喚醒後面的 block 和保留住執行後的結果。之後對相同 symbol 的呼叫就不會再喚醒 block,而是直接回傳結果。而當進入每一個新 spec 時,let 方法後面接的 block 會再次被喚醒,symbol 引數也將重新被定義一次。
require "rails_helper"
RSpec.describe Project do
let(:project) { Project.new }
let(:task) { Task.new }
it "considers a project with no tasks to be done" do
expect(project).to be_done
end
it "knows that a project with an incomplete task is not done" do
project.tasks << task
expect(project).not_to be_done
end
end
在以上程式碼範例:使用了兩次 let 方法,分別是用來描述 project 及 task。 請注意到即使是在第一個 spec 中沒有用到 task 也是沒有關係的,因為 let 方法後面接的區塊只有在該變數被使用時,才會被喚醒。實際上,let 方法是一種語法糖衣,其原形如下:
def me
@me ||= User.new(name: "Noel")
end
這裡要記住的是 let 後面接的區塊,除非是被喚醒,不然是不會執行的。這樣的 lazy load 特質對測試來說是好事,因為就不會浪費時間在建立不必要的物件。
另外 RSpect 還提供 let! 去執行後面接的 block,不論在 spec 內是否有使用到變數。
Dynamic matchers在 RSpec 無法識別該 matcher 的情況下,RSpec 會利用名稱修飾(name-mangling)的技巧,建立所謂的『 implicit matcher 』。
Predicate method
舉例來說,任何 matcher 長得像 be_whatever 或是 be_a_whatever,RSpec 會假設這兩種 matchers 都會擁有ㄧ個相關 matcher ,叫做 whatever?。 這個 whatever? 就稱為 predicate method。 如果 matcher 是被 to 呼叫,那通過測試的條件就是從 predicate method 得到回傳值 true。反之,如果 matcher 是被 not_to 呼叫,那通過測試的條件會是從 predicate method 得到回傳值是 false。
還記得在先前的範例中,程式碼中有 expect(project.done?).to be_truthy。 其實這個 done? 方法就是predicate method,所以作者可以將這個 RSpec expectation 重新改寫成較為口語化的編碼: expect(project).to be_done。
另外一點就是 RSpec 允許使用 and 和 or 將 不同的 matcher 連接一起。
expect(actual).to include("a").and match(/.*3.*/)
expect(actual).to eq(3).or eq(5)
你也可以將 matchers 當作引數傳給其他的 matchers,又或者是創作 matchers 以 match 來處理全部的資料結構。
1 expect(actual[0]).to eq(5)
2 expect(actual[1]).to eq(7)
3 expect(actual).to match([an_object_eq_to(5), an_object_eq_to(7)])
第三行的 RSpec expectation,在 actual 陣列中的每ㄧ個元素都會拿來與 an_object_eq_to(5) 跟 an_object_eq_to(7),也可以簡化成 eq(5) and eq(7) 做配對比較。 然而大部分 RSpec 內建的 matchers 都有相似的語法,可讓測試編碼更口語化。 舉例來說:『 expect(actual).to all(be_truthy) 』, 這個 all matcher 會拿 matcher 引數(這裡指的是 be_truthy ),然後再與 actual 陣列中的每個元素做配對。
Comments