理解unittest測試框架(五)——載入模組
- 2020 年 4 月 6 日
- 筆記
背景
前面一系列文章研究了unittest框架的一些最小單元,比如用例,結果,這次看的是載入模組,也就是測試用例,是如何被框架載入到的。
TestLoader
載入模組實際上就是TestLoader這個類。
- TestLoader ---- loadTestsFromTestCase ---- loadTestsFromModule ---- loadTestsFromName ---- loadTestsFromNames
可以看到,在這個類中,最關鍵的就是以上幾個方法。從名字可以看出來,他們分別是從測試用例中載入測試內容,從模組中載入測試內容,從名字中載入測試內容。
再進一步的看程式碼,可以發現loadTestsFromNames方法實際上調用的是loadTestsFromName方法。
程式碼如下
def loadTestsFromNames(self, names, module=None): """Return a suite of all test cases found using the given sequence of string specifiers. See 'loadTestsFromName()'. """ suites = [self.loadTestsFromName(name, module) for name in names] return self.suiteClass(suites)
所以,我們從loadTestsFromName開始看。
loadTestsFromName
這個方法有一個入參,也就是name,這個name是我們執行命令行啟動時,這樣的內容:
python -m unittest a.b.c.test
而這個a.b.c.test就是這個name。
if module is None: parts_copy = parts[:] while parts_copy: try: module = __import__('.'.join(parts_copy)) break except ImportError: del parts_copy[-1] if not parts_copy: raise這裡程式碼可以看到,如果傳入了module,那麼使用的就是這個module。如果沒有,那麼通過傳入的name來解析出module,這裡的__import__是一個動態引用的方法,如果引入失敗,那麼就把最後一層的數據踢掉重新引入,直到引入成功為止。
obj = module for part in parts: parent, obj = obj, getattr(obj, part)
接下來,通過循環的方式,找出最終執行的一個obj。
if isinstance(obj, types.ModuleType): return self.loadTestsFromModule(obj) elif isinstance(obj, type) and issubclass(obj, case.TestCase): return self.loadTestsFromTestCase(obj) elif (isinstance(obj, types.UnboundMethodType) and isinstance(parent, type) and issubclass(parent, case.TestCase)): name = parts[-1] inst = parent(name) return self.suiteClass([inst]) elif isinstance(obj, suite.TestSuite): return obj elif hasattr(obj, '__call__'): test = obj() if isinstance(test, suite.TestSuite): return test elif isinstance(test, case.TestCase): return self.suiteClass([test]) else: raise TypeError("calling %s returned %s, not a test" % (obj, test)) else: raise TypeError("don't know how to make test from: %s" % obj)找到這個obj之後,就要對這個obj進行判定。
如果這個obj是一個module類型。說明這裡測試的是一整個模組。那麼就調用loadTestsFromModule去載入測試的內容
如果這個obj是TestCase的子類,那麼說明這裡是一個測試類,調用loadTestsFromTestCase去載入測試的內容。
如果這個obj是一個方法,而parent是TestCase的子類。那麼直接用suiteClass來組織用例後再返回.
如果obj是一個TestSuite類型,那麼就直接返回這個類型即可。
以上幾種情況都是使用unittest自己的方法來寫的測試用例。還有可能用例是自己寫了call的方法,unittest還需要對這些做一下兼容。
也就是判定這個obj的call方法是不是TestSuite或者TestCase的類型,如果是的話,也要吧這部分數據通過一定的方式組織後返回。
從這裡其實可以看到,這裡載入完的測試數據,都是按照TestSuite的方式組織後返回的。
loadTestsFromTestCase
def loadTestsFromTestCase(self, testCaseClass): """Return a suite of all test cases contained in testCaseClass""" if issubclass(testCaseClass, suite.TestSuite): raise TypeError("Test cases should not be derived from TestSuite." " Maybe you meant to derive from TestCase?") testCaseNames = self.getTestCaseNames(testCaseClass) if not testCaseNames and hasattr(testCaseClass, 'runTest'): testCaseNames = ['runTest'] loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames)) return loaded_suite這裡的程式碼不多,類型判定沒問題之後,會調用getTestCaseNames這個方法獲取測試用例的名字,
getTestCaseNames這裡的程式碼不繼續貼了,有興趣的可以自己去看看,這裡主要做了幾件事:
- 把
test開頭的函數全部整理出來 - 按照ASCII碼的順序排列之後返回
再往下,如果沒有整理出test開頭的,但是有runTest方法在裡面,也就是自定義的測試用例。那麼也吧這部分給放到suite中返回。
loadTestsFromModule
tests = [] for name in dir(module): obj = getattr(module, name) if isinstance(obj, type) and issubclass(obj, case.TestCase): tests.append(self.loadTestsFromTestCase(obj))
這裡的邏輯其實也不複雜,從module中讀取所有的文件,再從文件中讀取所有的類,如果是TestCase的子類,那麼就調用loadTestsFromTestCase方法去載入數據。
load_tests = getattr(module, 'load_tests', None) tests = self.suiteClass(tests) if use_load_tests and load_tests is not None: try: return load_tests(self, tests, None) except Exception, e: return _make_failed_load_tests(module.__name__, e, self.suiteClass)
同樣的,這裡還有自定義載入模組的一些內容,如果模組中有load_tests來標記載入的內容,那麼就按照load_test的內容來載入用例。
discover
loader模組還有一個discover的函數,這個函數是用來尋找當前路徑下所有的測試用例,這個函數的思路和上面是類似的,獲取當前地址的絕對地址後,動態的引入,找到test*.py這樣的文件,再載入其中的測試用例,最後使用Suite的形式返回。
由於邏輯類似,這裡就不單獨的去展開,有興趣的朋友可以自行閱讀。
總結
unittest的載入模組是一個非常值得學習的源碼。從它的設計上來看,整個載入的最終結果,是按照Suite返回,原子方法就是TestCase的子類載入測試用例。而業務上各種方式也最終回歸到TestCase上,由TestCase來載入出數據,最終統一返回Suite。這中方式很值得我們在日常的開發中借鑒。

