天天看點

了解unittest測試架構(二)架構核心——case

背景

上文說到

unittest

架構的入口,知道了多種方式執行

unittest

的時候,架構是如何處理的。

本文會詳細說明測試架構的核心,

case

:測試用例是如何構成的。

從使用來感覺

我們在編寫測試用例的有這麼幾步

  1. 需要定義一個類,這個類名以

    Test

    開頭,并且需要繼承

    unttest.TestCase

  2. 定義好這個類之後,在這個類中定義以

    test

    開頭的函數。
  3. 在這個函數中編寫用例

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()           

複制

在設計架構的時候,經常會使用到這種機制。相比于第一種方式來看,這種方式更加靈活。即使不知道傳入的類是什麼。隻要這個類有對應的方法就可以執行。