理解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
。這中方式很值得我們在日常的開發中借鑒。