背景
上文說到
unittest
架構的入口,知道了多種方式執行
unittest
的時候,架構是如何處理的。
本文會詳細說明測試架構的核心,
case
:測試用例是如何構成的。
從使用來感覺
我們在編寫測試用例的有這麼幾步
- 需要定義一個類,這個類名以
開頭,并且需要繼承Test
。unttest.TestCase
- 定義好這個類之後,在這個類中定義以
開頭的函數。test
- 在這個函數中編寫用例
unittest
測試架構最終會把函數當成一條測試用例去執行。
在執行的過程中,
unittest
測試架構會根據參數來列印不同詳細程度的執行日志。
執行完畢後,列印出執行結果是成功或者是失敗。
同時,在執行用例主體前,會執行
setUp()
初始化函數,執行用例完畢後,會執行
tearDown()
清理函數。
如果有多條用例,那麼就會一直重複以上步驟。
case的構成
以上的核心就在
TestCase
這個基類。
從代碼的結構來看,
TestCase
除了主體的功能,有很大一部分都是斷言類的方法。斷言類的方法不多展開描述,主要看主體的功能。
- init
在測試的初始化中,主要的功能就是定義一些全局的資訊,比較關鍵的有兩個點。
第一,入參中傳入
methodName
,預設值是
runTest
。
實際上,在測試套件建立測試用例的時候,會把測試用例的名稱(test開頭的函數名))傳進來,這裡後面會用到。
第二,定義了一個清理的容器。
這個在後續,會吧清理方法丢進來,按順序的去執行清理的工作。
其他功能都是一些斷言的方法,或者聲明了空的鈎子方法。無需特别關注。
- run
這個是需要重點關注的,整個測試執行的編排就是通過這個鈎子函數來執行的。我們一段一段來看。
orig_result = result
if result is None:
result = self.defaultTestResult()
startTestRun = getattr(result, 'startTestRun', None)
if startTestRun is not None:
startTestRun()
self._resultForDoCleanups = result
result.startTest(self)
testMethod = getattr(self, self._testMethodName)
複制
最上面的部分是聲明了測試結果的對象。這裡會調用結果對象的鈎子函數,告訴結果對象,測試用例開始執行了,結果對象會通過标準輸出的方式把用例啟動的資訊打出來,關于結果對象,我們後續再看。
第二部分是把
init
方法中的
testMethodName
這個對象動态的加載進來,讀者可以用
debug
模式在這裡打一個斷點。上文也提到了。這個參數其實就是函數名。通過
getattr
方法加載進來這個對象。
接着往下看。
if (getattr(self.__class__, "__unittest_skip__", False) or
getattr(testMethod, "__unittest_skip__", False)):
# If the class or method was skipped.
try:
skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
or getattr(testMethod, '__unittest_skip_why__', ''))
self._addSkip(result, skip_why)
finally:
result.stopTest(self)
return
複制
這一部分是檢查測試用例是否被跳過了,如果發現用例需要跳過,那麼就把跳過的原因放到結果對象去,結果對象會把這些資料通過标準輸出的方式打到控制台。
在
unittest
中,跳過測試用例的方式是通過
@unittest.skip()
的裝飾器來實作跳過執行的,這個
skip
方法也是在
TestCase
所屬的
case.py
檔案中。
def skip(reason):
"""
Unconditionally skip a test.
"""
def decorator(test_item):
if not isinstance(test_item, (type, types.ClassType)):
@functools.wraps(test_item)
def skip_wrapper(*args, **kwargs):
raise SkipTest(reason)
test_item = skip_wrapper
test_item.__unittest_skip__ = True
test_item.__unittest_skip_why__ = reason
return test_item
return decorator
複制
這個
skip
的方法,實際上就是給這個測試對象添加
__unittest_skip__
和
__unittest_skip_why__
這兩個屬性。而測試用例是否跳過執行,就是從測試對象中拿這兩個參數來對比。
這些檢查做完之後,則是測試用例開始執行的代碼了。代碼有點長,我們一段一段來看。
try:
self.setUp()
except SkipTest as e:
self._addSkip(result, str(e))
except KeyboardInterrupt:
raise
except:
result.addError(self, sys.exc_info())
複制
這一部分的代碼,處理的是目前對象的
setUp()
方法。在
TestCase
中,這個
setUp()
是一個空方法,我們寫的時候如果寫了
setUp()
方法,那麼就相當于在子類中重寫了
setUp()
,當然,不管怎樣,這個方法都會優先于測試用例本身的代碼執行。
這裡監聽了幾個異常,如果有抛錯跳過,那麼就停止執行,如果監聽到了
KeyboardInterrupt
異常,這個異常實際上就是我們在執行的時候按下
Ctrl+C
的信号引發的異常。如果有其他異常,同樣的會停止執行。
try:
testMethod()
except KeyboardInterrupt:
raise
except self.failureException:
result.addFailure(self, sys.exc_info())
except _ExpectedFailure as e:
addExpectedFailure = getattr(result, 'addExpectedFailure', None)
if addExpectedFailure is not None:
addExpectedFailure(self, e.exc_info)
else:
warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
RuntimeWarning)
result.addSuccess(self)
except _UnexpectedSuccess:
addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
if addUnexpectedSuccess is not None:
addUnexpectedSuccess(self)
else:
warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
RuntimeWarning)
result.addFailure(self, sys.exc_info())
except SkipTest as e:
self._addSkip(result, str(e))
except:
result.addError(self, sys.exc_info())
else:
success = True
複制
這裡邏輯其實也不複雜,
testMethod
這個對象在
run
方法開始的時候,就被指派了測試函數作為對象,這裡則是執行這個對象。這裡有一大堆的異常捕獲,有興趣的讀者可以慢慢的去跟蹤這些異常。執行完畢如果沒有發現異常,則把
success
置為
True
。
用例執行完畢之後需要執行清理函數。
try:
self.tearDown()
except KeyboardInterrupt:
raise
except:
result.addError(self, sys.exc_info())
success = False
複制
這裡的邏輯基本上與
setUp()
類似。值得注意的是,如果清理函數執行失敗了,用例也會被當成失敗的。
如果根據平時寫用例來看,到這裡似乎流程就跟蹤完了。實際上我們看代碼之後發現,還沒有結束。
cleanUpSuccess = self.doCleanups()
success = success and cleanUpSuccess
if success:
result.addSuccess(self)
複制
這裡還調用了一次
doCleanups()
方法。
def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after
tearDown."""
result = self._resultForDoCleanups
ok = True
while self._cleanups:
function, args, kwargs = self._cleanups.pop(-1)
try:
function(*args, **kwargs)
except KeyboardInterrupt:
raise
except:
ok = False
result.addError(self, sys.exc_info())
return ok
複制
這裡會循環的把
self._cleanups
中的對象彈出來執行,彈出的資料是這個數組的最後一個對象給彈出來執行。是以自定義清理函數的執行原則是後進先出。
上面的這些執行邏輯統一包在一個
try
裡面,執行了這些之後,不論結果如果,都會執行最後的工作。
finally:
result.stopTest(self)
if orig_result is None:
stopTestRun = getattr(result, 'stopTestRun', None)
if stopTestRun is not None:
stopTestRun()
複制
最後的工作實際上就是結果對象的标準輸出了。
FunctionTestCase
在
case.py
檔案中,還有一個這個我們基本上很少見到的方法。它同樣是
TestCase
的子類。
這個方法的作用其實是一個裝飾器,通過這個裝飾器,可以吧一個已有的函數變成測試架構相容的函數,通過源代碼我們可以看到這個類中有這樣一個方法.
def runTest(self):
self._testFunc()
複制
而在上文中有提到,
TestCase
方法的
methodName
這個參數的預設值是
runTest
。而如果是一個正常的測試用例,這個參數會被傳入函數名。而使用這個修飾器的函數,傳入的就是預設值。是以在
run
中執行的
testMethodName
就是這個
runTest
方法。
總結
本文介紹了
unittest
測試架構中的測試用例是如何運作的。
再次回顧一下,測試用例首先呢通過初始化的時候傳入用例名(測試函數名)。通過編排,順序執行
setUp()
、測試用例主體邏輯、
tearDown()
以及個性化的
doCleanups
。完成一條用例的執行。
補充
- 關于
方法getattr
這個方法其實非常常見,可以簡單的了解為
反射
、
hook
或者
代理
。通過這種方式可以拿到對象的某個方法的執行個體。再通過調用的方式來執行對象中的方法。
例如如下代碼:
class A(object):
def test_attr():
print("hello")
複制
這樣的一個類,下面兩種方式,執行的結果是樣的:
a = A()
a.test_attr()
a = getattr(A, 'test_attr')
a()
複制
在設計架構的時候,經常會使用到這種機制。相比于第一種方式來看,這種方式更加靈活。即使不知道傳入的類是什麼。隻要這個類有對應的方法就可以執行。