章節概述#
本章實作 TestSuite,讓多個測試能被組合在一起執行並取得集體結果。這是 Composite 模式的經典應用——單一測試和測試群組對外暴露相同的介面。實作過程中,作者引入了 Collecting Parameter 模式來解決介面一致性的問題。
動機:消除重複的測試呼叫#
在之前的章節末尾,我們執行所有測試的方式相當粗糙:
print TestCaseTest("testTemplateMethod").run().summary()
print TestCaseTest("testResult").run().summary()
print TestCaseTest("testFailedResultFormatting").run().summary()
print TestCaseTest("testFailedResult").run().summary()這些程式碼充滿了重複。但重複不只是壞味道,它其實是尚未發現的設計元素的信號。我們需要的是將多個測試組合起來、一起執行並取得集體結果的能力。
先寫測試:TestSuite 的目標介面#
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
result = suite.run()
assert("2 run, 1 failed" == result.summary())這個測試描述了我們期望的 API:建立一個 TestSuite,加入測試,執行後取得結果摘要。
實作 TestSuite#
add() 方法很直覺——將測試加入一個 list:
class TestSuite:
def __init__(self):
self.tests = []
def add(self, test):
self.tests.append(test)run() 方法稍微棘手。我們希望所有測試共用同一個 TestResult,因此需要:
def run(self):
result = TestResult()
for test in self.tests:
test.run(result)
return result介面一致性問題:Composite 的核心約束#
Composite 模式的主要約束是:集合體必須和個別項目回應相同的訊息。如果 TestSuite.run() 接受一個 result 參數,那麼 TestCase.run() 也必須接受同樣的參數。
作者考慮了三個方案:
- Python 預設參數機制 — 不可行,因為預設值在編譯時求值,我們不想重複使用同一個
TestResult - 拆成兩個方法(一個分配 TestResult、一個執行測試)— 想不出好名字,暗示這不是好策略
- 由呼叫端分配 TestResult — 採用此方案
這個模式稱為 Collecting Parameter:由呼叫端建立收集器物件,傳入被呼叫端來累積結果。
套用 Collecting Parameter 模式#
測試修改為:
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
result = TestResult()
suite.run(result)
assert("2 run, 1 failed" == result.summary())run() 不再回傳值,TestSuite 和 TestCase 的介面一致了:
# TestSuite
def run(self, result):
for test in self.tests:
test.run(result)
# TestCase
def run(self, result):
result.testStarted()
self.setUp()
try:
method = getattr(self, self.name)
method()
except:
result.testFailed()
self.tearDown()classDiagram
class TestSuite {
-tests[]
+add(test)
+run(result)
}
class TestCase {
-name
+run(result)
+setUp()
+tearDown()
}
class TestResult {
+testStarted()
+testFailed()
+summary()
}
TestSuite o-- TestCase : contains
TestSuite o-- TestSuite : contains
TestCase ..> TestResult : reports to
TestSuite ..> TestResult : reports to修復既有測試#
介面改變後,四個既有測試因為使用了舊的無參數 run() 而失敗。修復方式是在每個測試中建立 TestResult 並傳入:
def testTemplateMethod(self):
test = WasRun("testMethod")
result = TestResult()
test.run(result)
assert("setUp testMethod tearDown " == test.log)
def testResult(self):
test = WasRun("testMethod")
result = TestResult()
test.run(result)
assert("1 run, 0 failed" == result.summary())
def testFailedResult(self):
test = WasRun("testBrokenMethod")
result = TestResult()
test.run(result)
assert("1 run, 1 failed" == result.summary())
def testFailedResultFormatting(self):
result = TestResult()
result.testStarted()
result.testFailed()
assert("1 run, 1 failed" == result.summary())用 setUp 消除重複#
每個測試都在分配 TestResult——這正是 setUp() 要解決的問題。重構後:
def setUp(self):
self.result = TestResult()
def testTemplateMethod(self):
test = WasRun("testMethod")
test.run(self.result)
assert("setUp testMethod tearDown " == test.log)
def testResult(self):
test = WasRun("testMethod")
test.run(self.result)
assert("1 run, 0 failed" == self.result.summary())
def testFailedResult(self):
test = WasRun("testBrokenMethod")
test.run(self.result)
assert("1 run, 1 failed" == self.result.summary())
def testFailedResultFormatting(self):
self.result.testStarted()
self.result.testFailed()
assert("1 run, 1 failed" == self.result.summary())
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
suite.run(self.result)
assert("2 run, 1 failed" == self.result.summary())清理測試呼叫端#
有了 TestSuite,測試的執行程式碼也變得乾淨了:
suite = TestSuite()
suite.add(TestCaseTest("testTemplateMethod"))
suite.add(TestCaseTest("testResult"))
suite.add(TestCaseTest("testFailedResultFormatting"))
suite.add(TestCaseTest("testFailedResult"))
suite.add(TestCaseTest("testSuite"))
result = TestResult()
suite.run(result)
print result.summary()補充: 這裡仍有重複——手動逐一加入每個測試方法。如果能從 TestCase 類別自動建構 TestSuite,就能消除這個重複。作者將此項留給讀者自行練習。
待辦清單更新#
Invoke test methodInvoke setUp firstInvoke tearDown afterwardInvoke tearDown even if the test method failsRun multiple testsReport collected resultsLog string in WasRunReport failed tests- Catch and report setUp errors
- Create TestSuite from a TestCase class
本章小結#
注意: 作者承認本章違反了一次規則——在測試尚未通過的情況下就寫了部分實作。如果你在閱讀時就發現了,給自己加分。理論上應該有一個簡單的 Fake Implementation 可以先讓測試通過,但作者當時沒想到。
本章完成了以下工作:
- 為
TestSuite撰寫測試 - 實作
TestSuite,使用 Composite 模式讓單一測試和測試集合共用相同介面 - 引入 Collecting Parameter 模式,由呼叫端分配
TestResult並傳入 - 修改
run()的介面以維持一致性,然後修復所有受影響的測試 - 利用
setUp()提取出共同的初始化邏輯