TestLoader源码解析
Posted pythontest
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TestLoader源码解析相关的知识,希望对你有一定的参考价值。
1 def loadTestsFromTestCase(self, testCaseClass) #看名称分析:从TestCase找测试集--那么就是把我们的def用例加载到testSuit里面 2 def loadTestsFromModule(self, module, *args, pattern=None, **kws) #看名称分析:从模块里面找测试集,那么 模块>类>test_方法>添加到testSuit里面 3 def loadTestsFromName(self, name, module=None) #看名称分析: 接收到name直接添加到testSuit里面 4 def loadTestsFromNames(self, names, module=None) #看名称分析:接受到的是一个包含测试test_方法的列表 5 def getTestCaseNames(self, testCaseClass) # 看名称分析: 取出一个包含test_方法的列表 6 def discover(self, start_dir, pattern=‘test*.py‘, top_level_dir=None) ## 看名称分析:发现--找test_方法 7 def _get_directory_containing_module(self, module_name) #获取目录包含的模块 8 def _get_name_from_path(self, path) #从路径从找名称 9 def _get_module_from_name(self, name) #从名称找模块 10 def _match_path(self, path, full_path, pattern) #正则匹配路径--参数包含pattern 那估计是匹配我们测试脚本格式的 11 def _find_tests(self, start_dir, pattern, namespace=False) #找测试集合 12 def _find_test_path(self, full_path, pattern, namespace=False) #找测试集合的路径
1 那就是1234 2 一个discover,getTest,_match_path 3 二个find 4 三个_get 5 四个loadTests 6 7 discover 逻辑 8 > 9 _find_tests【两个处理逻辑 一个是本次传的目录和上次传的一样或不一样,】 10 【一样:直接从我们传的目录下面继续去找testcaose---】 11 【不一样:会从我们传的目录下面去执行os.path.listdir找到所有的子文件列表paths(文件)),然后遍历得到单独的path做 start_dir+path拼接】 12 > 13 ①—get_name_from_path【传入start_dir,判断当前传入的目录是否为上次传入的顶级目录返回".",不一样可能有点绕-并返回一个值这个值有四种情况 . test ...test dir.tests--正常应该是返回test文件名 14 ②_find_test_path(self, full_path, pattern, namespace=False) 15 【执行这个从路径中找test,那么很明显 一样:传目录路径 不一样传文件路径 】 16 _find_test_path
1 class TestLoader(object): 2 """ 3 This class is responsible for loading tests according to various criteria 4 and returning them wrapped in a TestSuite 5 """ 6 testMethodPrefix = ‘test‘ 7 sortTestMethodsUsing = staticmethod(util.three_way_cmp) 8 suiteClass = suite.TestSuite 9 _top_level_dir = None 10 11 def __init__(self): 12 super(TestLoader, self).__init__() 13 self.errors = [] 14 # Tracks packages which we have called into via load_tests, to 15 # avoid infinite re-entrancy. 16 self._loading_packages = set() #这里创建了一个空的self._loading_packages={}无序且不重复的元素集合
1.discover方法:unittest.defaultTestLoader
a.定义了三个布尔值属性 is_not_importable==True则是不能导入,is_namespace ,set_implicit_top
b.对顶层目录做了处理--当服务首次启动 执行unittest.defaultTestLoader.discover("文件目录A",pattern,top_level_dir=None):self._top_level_dir = top_level_dir = start_dir 这三个相等。
再次执行unittest.defaultTestLoader.discover("文件目录B",pattern,top_level_dir=None): top_level_dir=self._top_level_dir【也就是他会默认上次start_dir 为顶层目录】--
无论是首次还是复次--上面的操作完成之后 self._top_level_dir = top_level_dir 仍然继续执行了一句这个---也就是说 self._top_level_dir == top_level_dir 始终一样
c.针对顶层目录不是一个目录文件做了一系列的处理如果你传的目录是一个可导入的模块-他在这个异常处理中.会重新自导入这个模块。并开始追寻他的绝对路径,判断其模块的可用性,然后执行_find_tests()寻找用例
d.如果是一个目录就直接开始执行_find_tests()寻找用例
e.所以他这里分两种情况可以找到用例 第一种:传的目录 第二种:传入的可导入模块-这种情况self._top_level_dir 最终也是一个绝对路径
1 def discover(self, start_dir, pattern=‘test*.py‘, top_level_dir=None): #一般我们top_level_dir传的都None 2 set_implicit_top = False #是否存在顶层目录 3 if top_level_dir is None and self._top_level_dir is not None: 4 # make top_level_dir optional if called from load_tests in a package 5 top_level_dir = self._top_level_dir #复次走这里 6 elif top_level_dir is None: #初次走这里 7 set_implicit_top = True 8 top_level_dir = start_dir 9 #上面这一串花里胡哨的东西就是处理顶层目录-如果是第一次启动服务- 10 #就走elif-top_level_dir==我们下面传的值--之后--self._top_level_dir就不为空了, 11 #但是top_level_dir 顶部是处理==None所以会走if=True 12 top_level_dir = os.path.abspath(top_level_dir)#转绝对路径 13 if not top_level_dir in sys.path: 14 #这里是防止重复将top_level_dir加入执行目录--BUT如果我第一次传的start_dir=a,第二次传的start_dir=b 15 #分析一下--第一次就是把a加入到了执行目录---下面self._top_level_dir=a 二次(复次)传b的时候,会出现top_level_dir=a---并没有判断b是否在执行目录 16 #这里加一波问号????????????????????? 17 #但是一般情况 我们目录就只有一个--所以这里--先放着。。。先看后面再来看这里 18 # all test modules must be importable from the top level directory 19 # should we *unconditionally* put the start directory in first 20 # in sys.path to minimise likelihood of conflicts between installed 21 # modules and development versions? 22 sys.path.insert(0, top_level_dir) 23 self._top_level_dir = top_level_dir 24 #如果top_level_dir我们传的目录不在可执行目录--则临时添加进去 25 is_not_importable = False #是否 不能导入 26 is_namespace = False #is_namespace那么这个字段的意思就是是否可以找到传入的路径 27 tests = [] 28 if os.path.isdir(os.path.abspath(start_dir)): #判断我们传的是否为一个目录--实际上这里直接用top_level_dir不香吗-- 29 start_dir = os.path.abspath(start_dir) 30 # 之前把top_level_dir = start_dir 31 # 然后top_level_dir = os.path.abspath(top_level_dir) 32 #现在start_dir = os.path.abspath(start_dir) 33 #为什么不 直接用top_level_dir? 小朋友你是否有许多问号 34 # 问题出在上面--复次的时候--并没有走 top_level_dir = start_dir 而是走的 top_level_dir = self._top_level_dir , 35 #所以如果我们上次传的路径如果和这次不一样--那么top_level_dir是不等于start_dir--而start_dir才是我们传的-- 36 if start_dir != top_level_dir: #所以这里相当于判断前后传的路径是否一样--一般来说我们的start_dir都是等于top_level_dir的 37 is_not_importable = not os.path.isfile(os.path.join(start_dir, ‘__init__.py‘))#如果是一个文件返回false 38 #如果不一样--则判断我们当前传入的start_dir/__init__.py是不是一个正确的文件路径..os.path.isfile() 返回布尔值 39 else: #如果我们传入的不是一个目录,就开始一堆花里胡哨的报错东西了。。暂时不用管 40 # support for discovery from dotted module names 41 try: 42 __import__(start_dir) 43 #这里就很有意思了---__impor__("PyFiles.Besettest")那就是导入PyFiles 44 #那么也就是说这个97.33的概率会报错--也就是说你如果目录错了--下面的else基本不会走。。。除非你很神奇的填的路径右侧是一个可导入的模块 45 except ImportError: 46 is_not_importable = True #如果导入不鸟--就is_not_importable设置为true 在这里我清楚了这个字段的含义--不能导入=true 47 else:#那么这里假设导入成功之后 48 the_module = sys.modules[start_dir] #这里是如果我们导入成功--就走这里-取出start_dir导入的赋值给the_module 49 top_part = start_dir.split(‘.‘)[0] #这里是将我们导入的模块名称取出来 50 try: 51 start_dir = os.path.abspath( #打印导入模块所在目录的绝对路径 52 os.path.dirname((the_module.__file__))) 53 except AttributeError: #这里是如果导入模块成功了---但是尼玛打印导入模块的绝对路径又报错--不想看了+2 54 # look for namespace packages 55 try: #然后有开始进行模块导入检查---日了狗了。。。。。 56 # fuck----想直接关机了+1,这里估计是想找到为什么不能导入的原因。。大神的思路就是完美,如果是我就抛出一个目录不对就完事 57 #这一块的学习 文档 Python标准模块--import 58 spec = the_module.__spec__ 59 #将导入成功的模块的规格说明赋值给spec-- 60 #打印出来就是ModuleSpec(name=‘besettest.interface‘, loader=<_frozen_importlib_external.SourceFileLoader object at 0x0000000003D6E780>, origin=‘E:\PyFiles\Besettest\besettest\interface\__init__.py‘, submodule_search_locations=[‘E:\PyFiles\Besettest\besettest\interface‘]) 61 #这么一串东西--也没用过。。。。大概就是模块名称、路径、导入的模块对象吧 62 #origin 加载模块的位置-- 63 #loader_state模块特定数据的容器 64 except AttributeError: #如果模块的规格说明取不出来。。。。。。。 65 spec = None #我查阅了一下。。。的确存在部分模块规格说明为None的--所以还得继续往下看 66 67 if spec and spec.loader is None: #如果存在规格说明 且 数据容器为None。 68 if spec.submodule_search_locations is not None: 69 #这是个什么玩意呢--模块 搜索 位置s(列表)。。。 70 is_namespace = True #如果spec.submodule_search_locations不为none ----- 71 # 2.5级英文翻译 就是模块的路径 如果模块路径不为空is_namespace可以找到---is_namespace那么这个字段的意思就是存在命名空间。。就是可以找到这个模块 72 for path in the_module.__path__: #这里我研究怀疑是故意提升逼格。。the_module.__path__==spec.submodule_search_locations 73 if (not set_implicit_top and #首次set_implicit_top==True 复次set_implicit_top==Fase 74 not path.startswith(top_level_dir)): 75 continue 76 #这里让我稍微有点疑惑。。为什么要判断是首次还是复次--我猜是判断the_module.__path__列表里面有几个某块的路径 77 #如果是首次直接下一步--如果是复次会有多个路径。但是如果是复次top_level_dir这个路径又是上次的。。。日 78 #他的作用是找到导入模块的路径-知道这个就行。。。 79 self._top_level_dir = 80 (path.split(the_module.__name__ 81 .replace(".", os.path.sep))[0]) #取出导入模块的上级目录绝对路径。。。 82 #the_module.__name__.replace(".", os.path.sep) 这一串我看来是没有必要的。。因为 the_module.__name__既然取到了模块名称那他肯定是一个字符串 83 tests.extend(self._find_tests(path, #然后调用_find_tests 寻找测试。加入tests列表--这个有点熟悉的味道--- 84 pattern, #我觉得基本不会走这里去找---脚本路径一般都会填对 填错了,都不知道执行到哪里去了。。。 85 namespace=True)) 86 elif the_module.__name__ in sys.builtin_module_names: 87 #判断 sys.builtin_module_names返回一个列表,包含所有已经编译到Python解释器里的模块的名字 和sys.models是一个字典 88 #就是没法导入报错 89 # builtin module 90 raise TypeError(‘Can not use builtin modules ‘ 91 ‘as dotted module names‘) from None 92 else: #没发现这个模块 93 raise TypeError( 94 ‘don‘t know how to discover from {!r}‘ 95 .format(the_module)) from None 96 97 if set_implicit_top: #如果是首次。。。。 98 if not is_namespace: #is_namespace默认的是false-且可以找到模块相关规格 99 self._top_level_dir = 100 self._get_directory_containing_module(top_part) #interface.testFiles interface假设这个是导入的 -self._top_level_dir 是一个目录的绝对路径 101 #top_part导入的模块名称----- 102 sys.path.remove(top_level_dir) #只知道是从系统路径移除--但是不知道为什么移除。。。。 103 else: 104 sys.path.remove(top_level_dir) # 105 106 if is_not_importable: #如果我们传的文件不能导入---就直接抛出异常 107 raise ImportError(‘Start directory is not importable: %r‘ % start_dir) 108 109 if not is_namespace: #is_namespace默认的是false--这里就是可以找到模块。。。。 110 tests = list(self._find_tests(start_dir, pattern)) 111 return self.suiteClass(tests)
2._find_tests()--寻找testCase并生成测试套件tests=[]
1 def _find_tests(self, start_dir, pattern, namespace=False): #注意这里如果我们传的不是脚本目录而是一个可导入的模块namespace是等于True的 2 """Used by discovery. Yields test suites it loads.""" 3 # Handle the __init__ in this package 4 name = self._get_name_from_path(start_dir) #返回一个name name存在三种返回情况 "."-当本次和上次传入的start_dir一致 不一致 "文件名" "...文件名" 5 #get_name_from_path的逻辑在这里就很清晰了 6 7 # name is ‘.‘ when start_dir == top_level_dir (and top_level_dir is by 8 # definition not a package). 9 if name != ‘.‘ and name not in self._loading_packages: 10 #当name最少有一个且也不再self._loading_packages.【self._loading_packages初始化的时候建的空集合】 走下面这个 11 # name is in self._loading_packages while we have called into 12 # loadTestsFromModule with name. 13 tests, should_recurse = self._find_test_path( #然后这里start_dir是我们传的模块--他就去找。。这里就恢复到了传测试目录的逻辑了 14 start_dir, pattern, namespace) 15 if tests is not None: 16 yield tests 17 if not should_recurse: 18 # Either an error occurred, or load_tests was used by the 19 # package. 20 return 21 # Handle the contents. 22 paths = sorted(os.listdir(start_dir)) #那就从这里开始--当我们穿的目录和上次一样-他会找到目录下所有的文件然后排序--我们的用例执行顺序就是从这里开始搞了。。 23 for path in paths: #遍历我们传的目录下的所有文件 24 full_path = os.path.join(start_dir, path) 将我们传入的目录和目录下的py文件拼接的完整路径 25 tests, should_recurse = self._find_test_path( #把我们文件路径和我们的文件格式传入_find_test_path这个方法-- 26 full_path, pattern, namespace) 27 如果当前传的是一个目录-会返回should_recurse=True--这个英文直译是应该_递归--下面yield from 就是执行递归的操作 28 if tests is not None: 29 yield tests 30 if should_recurse: #这句是判断他是不是一个目录 31 # we found a package that didn‘t use load_tests. 32 name = self._get_name_from_path(full_path) 33 self._loading_packages.add(name) 34 try: 35 yield from self._find_tests(full_path, pattern, namespace) 36 finally: 37 self._loading_packages.discard(name)
yield与yield from
1 def a(n): 2 testList=b(n) 3 return testList 4 5 def b(n,m=1): 6 print("执行第%s次"%m) 7 for a in range(n): 8 if not divmod(a,2)[1] and a!=0: 9 print(a) 10 yield a #是用yield之后返回的是一个生成器 11 if divmod(a,3)[1]: 12 m =m+1 13 yield from b(a,m) #重新执行b方法 14 15 print(list(a(7))) 16 17 执行第1次 18 2 19 执行第2次 20 4 21 执行第3次 22 2 23 执行第4次 24 6 25 [2, 4, 2, 6]
_get_name_from_path
他主要做了:从我们传的路径里面找脚本文件 名。。。如果找到了脚本文件则返回文件名称---如果没找到也就是我们返回一个点 或者 至少一个点(三种情况 . test_case .....test_case 返回name可能存在的三种值)
在_test_find调用这个方法path=start_dir(我们传的目录)--这个返回一个点 或者 至少一个点
在 _fin_test_path调用这个方法是传的我们传的目录下的文件路口--返回的name就是文件名
1 name = self._get_name_from_path(start_dir) #因为discover我们是支持我们传目录或者模块寻找testcase的,所以这个方法 2 3 def _get_name_from_path(self, path): 4 #主要正确逻辑三个 比如我们的脚本目录结构是 E://a/b/ b目录下面有script.py 和 /c/script.py 5 #第一次是我们自己传的目录--之前在discover他做了一个处理 就是第一次运行时会把我们传的目录赋值给顶层目录--- 6 #第一个逻辑判断我们传的是不是 -和顶层一样---一样的话===_find_tests方法就直接从目录下面找脚本-当如如果有目录也会继续走--他是在_find_tests_path判断的--最终也是回到找脚本模块上 7 #如果不一样--那就是找了 找到这个目录了---那么就从顶层开始找这个目录的相对路径--其实就是找最后那个目录(必须是一个packge。上面说的目录都是包)、。。然后返回一个name 8 #如果还有子目录 d---那就会返回 c.d 9 if path == self._top_level_dir: #首次运行pattern,top_level_dir=None):self._top_level_dir = top_level_dir = start_dir -第二次运行如果目录没有变,这里也是直接返回的 10 return ‘.‘ 11 12 13 path = _jython_aware_splitext(os.path.normpath(path)) #如果我们当前传的和上次传的目录不一致。。这里得path我们当前传的路径 14 15 _relpath = os.path.relpath(path, self._top_level_dir) #从self._top_level_dir开始找path的相对路径 16 #这里是从我们传的path开始找到self._top_level_dir上次传的相对路径 17 #例如: path=path1="E:\PyFiles\Besettest\besettest\interface\testFiles" self._top_level_dir="E:\PyFiles\Besettest\besettest\interface\result" 18 #那么_relpath=".. estFiles" -暂时还不清楚为什么要找这个?????????????????????????? 19 assert not os.path.isabs(_relpath), "Path must be within the project" 20 #↑↑断言 不是绝对路径-也就是说_relpath是否为相对路径↑↑↑特么的 这里肯定是一个相对路径啊。。。上面都有relpath了。。。丢 21 #↓↓↓↓断言以..开头就失败。。。↓↓↓--这两处超出理解范围了。。。。。。 22 assert not _relpath.startswith(‘..‘), "Path must be within the project" 23 24 name = _relpath.replace(os.path.sep, ‘.‘) #然后这里又把分隔符替换成. 返回 a.b 当然或许会有异常情况返回.....这是我意淫的 25 return name
self._find_test_path #找测试的路径
两个主要逻辑: 传到full_path 是一个文件 还是一个目录
1 def _find_test_path(self, full_path, pattern, namespace=False): 2 #_find_tests()调用这个方法 传了一个我们传的目录下的a文件路径、和需要找的文件pattern-namespace【传的可能是true 也可能是false】,如果我的目录是对的-namespace传的就是false 3 """Used by discovery. 4 5 Loads tests from a single file, or a directories‘ __init__.py when 6 passed the directory. 7 8 Returns a tuple (None_or_tests_from_file, should_recurse). 9 """ 10 basename = os.path.basename(full_path) #basename==文件名.py后续带py的统称文件--不带后缀的统称文件名。。。 11 if os.path.isfile(full_path): #如果我们传的full_path是一个文件---我们在discover传的是一个脚本目录-之前在_test_find是做了一个拼接得到的完整路径full_path 12 if not VALID_MODULE_NAME.match(basename): #判断他是不是一个py文件---- 13 14 # valid Python identifiers only 15 return None, False #如果不是直接返回 16 if not self._match_path(basename, full_path, pattern): #这里虽然传了三个值--但是实际上只有basename,pattern有用-- 17 #_match_path调用fnmatch(文件, 我们传的文件格式或文件)这个需要————from fnmatch import fnmatch他的主要作用是做此模块的主要作用是文件名称的匹配 18 #当此次传入的文件名与我们的文件格式匹配一致self._match_path返回true 19 return None, False #如果不一样 就直接回到 _find_test继续找 20 # if the test file matches, load it 21 name = self._get_name_from_path(full_path) #然后这里把文件路径又传到 self._get_name_from_path去返回文件名-这个时候因为我们传的是脚本目录-full_path目录下的文件路径,所以返回的name 就是文件名 22 23 #self._top_level_dir是当前文件目录路径,path是当前文件路径--从目录找文件--直接就是文件名--他返回的name就是文件名 24 try: 25 module = self._get_module_from_name(name) #_get_module_from_name 这个方法就是动态导入模块名--然后返回一个所有导入的模块的对象 moudel.__file__路径、moudel.__name__名称 26 except case.SkipTest as e: #如果导入不成功 case.SkipTest 实际上case是继承--Exception--所以把这个理解为Exception就可以了-- 27 return _make_skipped_test(name, e, self.suiteClass), False 28 except: 29 error_case, error_message = 30 _make_failed_import_test(name, self.suiteClass) 31 self.errors.append(error_message) 32 return error_case, False 33 else: #module 获取到值之后走这里。。 34 mod_file = os.path.abspath( 35 getattr(module, ‘__file__‘, full_path)) #然后这里取出我们导入模块的 绝对路径---如果反射找不到就返回该文件的路径-其实差别不大-处理一下更严谨 36 realpath = _jython_aware_splitext( 37 os.path.realpath(mod_file)) #os.path.realpath(mod_file)然后又返回真实路径---然后又去掉路径的.py,。,,,,,,,,丢 38 fullpath_noext = _jython_aware_splitext( 39 os.path.realpath(full_path)) #然后full_path 找真实路径去掉.py 40 if realpath.lower() != fullpath_noext.lower(): #如果动态导入的模块的目录路径 不等于 传进来(也就是pattern)的目录路径--实际上传进来的路径肯定是个绝对路径--因为前面已经转了好几次绝对路径了 41 module_dir = os.path.dirname(realpath) #不等于就找动态导入模块所在的目录----实际上上面处理的realpath已经是一个目录了。。但是他防止realpath还是一个.py文件。所以又操作了一次 42 mod_name = _jython_aware_splitext( #full_path是文件的路径.py的,然后这里又先是basename取出文件(就是把路径去掉,只留下xxx.py) 然后外面那个方法 把.py去掉--留下文件名 43 os.path.basename(full_path)) 44 expected_dir = os.path.dirname(full_path) 45 #然后找到需要执行脚本所在的目录。。。。。也就是说正常情况 假设expected_dir="e://a/b" 那么 mod_file =realpathfullpath_noext="e://a/b/scripy" 46 #scripy是一个py文件---上面这个if是说的 正常情况。。。我想不到导入模块和导入模块的路径不相等的情况--不过这个不重要-源码这样肯定是有道理的 47 msg = ("%r module incorrectly imported from %r. Expected " 48 "%r. Is this module globally installed?") 49 raise ImportError( 50 msg % (mod_name, module_dir, expected_dir)) 51 return self.loadTestsFromModule(module, pattern=pattern), False 52 #然后走 从模块从加载测试s 这个方法---也就是说discover实际上是调用loadTestsFromMould这个方法的。。测试套件也是在这一步处理的 53 elif os.path.isdir(full_path): #dicover里面传脚本目录是走这里。。 54 if (not namespace and #namespace-默认是false not namespace就是true 55 not os.path.isfile(os.path.join(full_path, ‘__init__.py‘))): #不是一个包。。-也就是说我们传的目录应该是一个包,下面包含__init__.py 56 return None, False 57 58 load_tests = None #这个load_tests是啥意思呢??????????后面继续看----看了一遍--并且用unittest.main()试了一下-模块下面是没有这个属性的。。只是unittest的初始化文件有这个方法--他也是通过discover找的。。 59 tests = None 60 name = self._get_name_from_path(full_path) #这里就是走子目录的逻辑了 61 #get_name_from_path的逻辑在这里就很清晰了 62 #A.如果通过_find_test 调用self._get_name_from_path 是为了判断两次start_dir是否一致一致返回. 不一致返回从上次的start_dir1找到本次start_dir12的相对路径-- 63 #这里又分两种情况-A1正常情况-start_dir1是start_dir12的上级目录。。。那么返回的那么就是-A.B这样的了。。因为第一次的A是已经os.path.insert到环境变量了..所以A.B是可以直接用 64 #A2不正常情况 就是之前说的 最少返回一个点的...A这种返回---然后问题来了--他为什么要这么处理呢--原因是????????? 65 #我猜是与脚本同级存在另一个脚本目录。。。后面验证这一点。-----这里在上面补充了--是因为子目录中还存在脚本所有这么走逻辑-完美的 66 try: 67 package = self._get_module_from_name(name) #上面是导入的一个module--这里是导入一个包-- 返回-- 68 except case.SkipTest as e: 69 return _make_skipped_test(name, e, self.suiteClass), False 70 except: 71 error_case, error_message = 72 _make_failed_import_test(name, self.suiteClass) 73 self.errors.append(error_message) 74 return error_case, False 75 else: 76 load_tests = getattr(package, ‘load_tests‘, None) #然后判断这个包里面有没有‘load_tests‘这个属性---这里我代码一直看下来,我们是不知道这个lood_tests是什么的,字面意思 加载测试集合 77 # Mark this package as being in load_tests (possibly ;)) 78 self._loading_packages.add(name) #然后把模块名称添加到set集合 79 try: 80 tests = self.loadTestsFromModule(package, pattern=pattern) 81 #这里传了一个package模块对象,和文件匹配规则合作或者文件。。--但是这里导入一个包之后实际上是找不到testCase的-因为包下面的属性肯定不是一个类-不会走loadTestsFromTestCase 82 #所以这里返回的tests是一个空列表 83 if load_tests is not None: #貌似这个是弃用的,向后兼容-暂时没看明白这个load_tests代表的意思 84 # loadTestsFromModule(package) has loaded tests for us. 85 return tests, False 86 return tests, True # 如果能走到这里------就返回True_就是给_find_test判断走递归的--_find_tests里面就得到 should_recurse=True 87 finally: 88 self._loading_packages.discard(name) #然后这个删掉set集合里面之前导入的那个包 89 else: 90 return None, False
1 def loadTestsFromModule(self, module, *args, pattern=None, **kws): 2 """Return a suite of all test cases contained in the given module""" 3 # This method used to take an undocumented and unofficial 4 # use_load_tests argument. For backward compatibility, we still 5 # accept the argument (which can also be the first position) but we 6 # ignore it and issue a deprecation warning if it‘s present. 7 if len(args) > 0 or ‘use_load_tests‘ in kws: #args这个默认是一个空元组 长度默认为0 kws是一个空字典 8 warnings.warn(‘use_load_tests is deprecated and ignored‘, 9 DeprecationWarning) 10 kws.pop(‘use_load_tests‘, None) 11 if len(args) > 1: 12 # Complain about the number of arguments, but don‘t forget the 13 # required `module` argument. 14 complaint = len(args) + 1 15 raise TypeError(‘loadTestsFromModule() takes 1 positional argument but {} were given‘.format(complaint)) 16 if len(kws) != 0: 17 # Since the keyword arguments are unsorted (see PEP 468), just 18 # pick the alphabetically sorted first argument to complain about, 19 # if multiple were given. At least the error message will be 20 # predictable. 21 complaint = sorted(kws)[0] #取出第一个Key- 22 raise TypeError("loadTestsFromModule() got an unexpected keyword argument ‘{}‘".format(complaint)) 23 tests = [] 24 for name in dir(module): #这里得modul实际上使我们传入的模块对象---dir(object) 返回模块下的所有属性列表- 25 obj = getattr(module, name) #然后反射返回name对象。。返回的是一个class对象 26 if isinstance(obj, type) and issubclass(obj, case.TestCase): #这里判断obj是否是一个类--并且这个类是case.TestCase的子类,也就是说 是否写在我们继承unitest.testCase那个类的下面 27 tests.append(self.loadTestsFromTestCase(obj)) #可以看到最后走loadTestFromTestCase obj这里是传入的一个类名 28 29 load_tests = getattr(module, ‘load_tests‘, None) 30 tests = self.suiteClass(tests) 31 if load_tests is not None: 32 try: 33 return load_tests(self, tests, pattern) 34 except Exception as e: 35 error_case, error_message = _make_failed_load_tests( 36 module.__name__, e, self.suiteClass) 37 self.errors.append(error_message) 38 return error_case 39 return tests #返回集合
loadTestsFromTestCase:这里就是添加testcase到suit集合里面的主要逻辑
1 def loadTestsFromTestCase(self, testCaseClass): #testCaseClass是我们传的一个用例类 2 """Return a suite of all test cases contained in testCaseClass""" 3 if issubclass(testCaseClass, suite.TestSuite): #这个类是不是suite.TestSuite的子类--如果是的就抛出异常== 4 raise TypeError("Test cases should not be derived from " 5 "TestSuite. Maybe you meant to derive from " 6 "TestCase?") 7 testCaseNames = self.getTestCaseNames(testCaseClass) #getTestCaseNames 从类下面找到用例名称--找到名称返回的是一个列表 8 if not testCaseNames and hasattr(testCaseClass, ‘runTest‘): #这里判断testCaseNames是否为空-并且 是否存在"runTest"这个元素 9 testCaseNames = [‘runTest‘] 10 loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames)) #是我的一个用例类--然后将testCaseNames类下面的测试方法--带进去遍历。。。高级用法---第一次见--这个方法很关键 11 return loaded_suite
getTestCaseNames
1 def getTestCaseNames(self, testCaseClass): 2 """Return a sorted sequence of method names found within testCaseClass 3 """ 4 def isTestMethod(attrname, testCaseClass=testCaseClass, #定义一个内部方法 5 prefix=self.testMethodPrefix): #self.testMethodPrefix="test" 这个在TestLoader下第一行就已经默认了,他是我们用例开头的固定格式 6 return attrname.startswith(prefix) and #这里判断了是否已test开头以及 方法对象是否可用----getattr返回方法对象--callable()是检测对象是否可用 返回一个布尔值 7 callable(getattr(testCaseClass, attrname)) 8 testFnNames = list(filter(isTestMethod, dir(testCaseClass))) 9 #dir(testCaseClass)返回该对象下面所有的属性--包括变量test_1--所以上面需要检测属性时test开头且是一个可以调用的对象-- 10 #filter函数---前面是一个function -后面是一个可迭代对象-会遍历可迭代对象-并传入function-functions返回true则添加到列表--这样就找到了所有的 11 if self.sortTestMethodsUsing: 12 testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing)) 13 #sortTestMethodsUsing = staticmethod(util.three_way_cmp) 转为为静态方法-内存地址指向self.sortTestMethodsUsing 14 #然后通过functools这个模块排序== 15 return testFnNames #然后返回用例方法名称列表
所有的逻辑
就是在TestLoader找测试用例的时候--通过_find_tests这个方法从目录开始找文件(子目录)-模块-类-方法名
然后将某个模块下的类通过他下面的方法map返回多个对象,也就是说一个testClass下面存在五个test_method,他就会返回五个实例对象-并生成一个suite集合--然后加入到一个列表
如果一个模块下有多个testClasee 同样-实际上是一样的--实际上他是先通过loadTestsFromModule这个方法找到所有的类对象之后在遍历--然后才走上面那一步的,,,多个testClasss就存在多个suite集合--
也就是说 一个modul下面的 suite集合会添加到一个列表--[suite=[A-TestCase实例化对象1,A-TestCase实例化对象2],suite=[B-TestCase实例化对象1,B-TestCase实例化对象2]]---然后在将这个列表当做参数传入TestSuite实例化一个新对象[suite=-[suite=[A-TestCase实例化对象1,A-TestCase实例化对象2],suite=[B-TestCase实例化对象1,B-TestCase实例化对象2]]]----这样就是一个模块下用的结构
但是还没有完--这里只是一个modul下的---还有多个modul--到了大家估计也知道剩下的会干什么了---
没错--当我得到modul的全部suite集合之后---这个集合最终会返回给_find_tests方法--通过生成器返回给discover--也就是将这个suite集合又加入到了一个新的列表--然后discover又将这个list
带入形成了一个-----最终的实例对象,最终返回的格式如下-------
[suite=
[suite1=-[suite=[A-TestCase实例化对象1,A-TestCase实例化对象2],suite=[B-TestCase实例化对象1,B-TestCase实例化对象2]]],
[suite2=-[suite=[A-TestCase实例化对象1,A-TestCase实例化对象2],suite=[B-TestCase实例化对象1,B-TestCase实例化对象2]]]
]
--看过源码的都知道---我们run的时候---就这个实例对象是可以接受参数的--而这个参数就是result---因为TestSuite继承的BaseTestsSuite 有一个__call__这个魔术方法:如果在类中实现了 __call__ 方法,那么实例对象也将成为一个可调用对象,具体百度。这里不做过多解释-----所以最终的suite是可以接受参数的test(result)--接受参数之后直接走——call下面的逻辑了
以上是关于TestLoader源码解析的主要内容,如果未能解决你的问题,请参考以下文章
片段(Java) | 机试题+算法思路+考点+代码解析 2023
Android 逆向类加载器 ClassLoader ( 类加载器源码简介 | BaseDexClassLoader | DexClassLoader | PathClassLoader )(代码片段
初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段
初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段