理解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。这中方式很值得我们在日常的开发中借鉴。