自我參照的挑戰#

用正在開發的測試工具來執行自身的測試,感覺就像對自己進行腦部手術。然而,測試框架的邏輯比 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

現在印出 NoneNone。我們要的是 None1,只需在 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()

技巧: 在每一步之間都執行測試,確保得到相同的結果。這是重構的基本紀律。

自動化測試#

已經厭倦了每次手動檢查印出的 None1。利用剛建立的機制,現在可以寫出自動化測試:

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