自我參照的挑戰#
用正在開發的測試工具來執行自身的測試,感覺就像對自己進行腦部手術。然而,測試框架的邏輯比 Part I 的 Money 範例複雜得多,這個過程可以視為邁向「真實」軟體 TDD 開發的一步,也可以視為一個自我參照程式設計(self-referential programming)的練習。
起始待辦清單#
首先,我們需要能夠建立一個 test case 並執行一個 test method,例如:TestCase("testMethod").run()。以下是測試框架的待辦清單:
- Invoke test method
- Invoke setUp first
- Invoke tearDown afterward
- Invoke tearDown even if the test method fails
- Run multiple tests
- Report collected results
Bootstrap 問題#
我們面臨一個 bootstrap 問題:我們需要撰寫 test case 來測試框架,但框架本身還不存在。因此,第一個小步驟的驗證必須手動完成。
策略是建立一個包含旗標(flag)的 test case。在 test method 執行前,旗標應為 false;test method 會設定旗標;執行後旗標應為 true。
手動驗證的第一步#
我們從手動呼叫 test method 開始:
test= WasRun("testMethod")
print test.wasRun
test.testMethod()
print test.wasRun預期先印出 None(Python 中代表 false),再印出 1。但這還不能執行,因為 WasRun 類別尚未定義。
先建立空的類別:
class WasRun:
pass接著需要在建構子(__init__)中建立 wasRun 屬性:
class WasRun:
def __init__(self, name):
self.wasRun= None執行後印出 None,然後告訴我們需要定義 testMethod:
def testMethod(self):
pass現在印出 None 和 None。我們要的是 None 和 1,只需在 testMethod() 中設定旗標:
def testMethod(self):
self.wasRun= 1現在得到正確答案——綠燈!
引入 run() 方法#
接下來需要使用真正的介面 run(),而非直接呼叫 test method:
test= WasRun("testMethod")
print test.wasRun
test.run()
print test.wasRun先用最簡單的方式實作:
def run(self):
self.testMethod()測試重新回到正確結果。
動態呼叫 test method#
下一步是動態呼叫 test method。Python 的強大特性之一是類別和方法的名稱可以當作函式使用。透過 getattr 取得對應名稱的屬性,就能將其作為函式呼叫:
class WasRun:
def __init__(self, name):
self.wasRun= None
self.name= name
def run(self):
method = getattr(self, self.name)
method()重點: 這裡展示了一個常見的重構模式——將在一個特定情境下運作的程式碼,透過用變數取代常數來泛化。此處的「常數」是寫死的程式碼而非資料值,但原則相同。TDD 透過提供具體可執行的範例來做泛化,而非純粹靠推理。
抽取 TestCase 超類別#
WasRun 類別同時負責兩件事:追蹤方法是否被呼叫,以及動態呼叫方法。是時候進行分離了。
首先建立空的 TestCase 超類別,讓 WasRun 成為子類別:
class TestCase:
pass
class WasRun(TestCase):
...將 name 屬性移到超類別:
class TestCase:
def __init__(self, name):
self.name= name
class WasRun(TestCase):
def __init__(self, name):
self.wasRun= None
TestCase.__init__(self, name)run() 方法只使用超類別的屬性,因此也搬移到超類別:
class TestCase:
def __init__(self, name):
self.name= name
def run(self):
method = getattr(self, self.name)
method()技巧: 在每一步之間都執行測試,確保得到相同的結果。這是重構的基本紀律。
自動化測試#
已經厭倦了每次手動檢查印出的 None 和 1。利用剛建立的機制,現在可以寫出自動化測試:
class TestCaseTest(TestCase):
def testRunning(self):
test= WasRun("testMethod")
assert(not test.wasRun)
test.run()
assert(test.wasRun)
TestCaseTest("testRunning").run()測試主體就是把 print 語句轉換成 assertion,本質上可以視為一種 Extract Method。
待辦清單更新:
Invoke test method- Invoke setUp first
- Invoke tearDown afterward
- Invoke tearDown even if the test method fails
- Run multiple tests
- Report collected results
本章回顧#
- 在幾次過於自信的嘗試失敗後,找到了一個足夠小的起步方式
- 透過先寫死再泛化(用變數取代常數)來實作功能
- 使用了 Pluggable Selector 模式(但承諾至少四個月內不再使用,因為它讓程式碼難以靜態分析)
- 用極小步驟完成了測試框架的 bootstrap