理解unittest測試框架(五)——載入模組

背景

前面一系列文章研究了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去載入測試的內容

如果這個objTestCase的子類,那麼說明這裡是一個測試類,調用loadTestsFromTestCase去載入測試的內容。

如果這個obj是一個方法,而parentTestCase的子類。那麼直接用suiteClass來組織用例後再返回.

如果obj是一個TestSuite類型,那麼就直接返回這個類型即可。

以上幾種情況都是使用unittest自己的方法來寫的測試用例。還有可能用例是自己寫了call的方法,unittest還需要對這些做一下兼容。

也就是判定這個objcall方法是不是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這裡的程式碼不繼續貼了,有興趣的可以自己去看看,這裡主要做了幾件事:

  1. test開頭的函數全部整理出來
  2. 按照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。這中方式很值得我們在日常的開發中借鑒。