unittest
— 单元测试框架 Unittest测试框架的的原理是将继承unittest.TestCase的测试类中,所有的test开头的测试函数,生成该测试类的一个对象,然后组装成测试套件,使用测试运行器(TestRunner)运行并使用测试结果(TestResult)对象纪录每个用例的运行状态。
基本架构
TestCase :测试用例
TestSuite:测试套件
TestLoader:测试用例加载器
TestResult:测试结果记录器
TestRunner:测试运行器
测试用例 (TestCase) 用例文件创建 用例执行顺序:并非按书写顺序执行,而是按用例名ascii码先后顺序执行
导入unittest模块
创建测试类,并继承unittest.TestCase
定义测试函数,函数名以test开头
用例类定义
用例方法定义
命令行界面执行当前测试类的用例 1 2 3 4 5 6 7 8 python -m unittest test_module1 test_module2 python -m unittest test_module.TestClass python -m unittest test_module.TestClass.test_method python -m unittest -v test_module
断言
方法
检查
新品
assertEqual(a, b)
a == b
assertNotEqual(a, b)
a != b
assertTrue(x)
bool(x) is True
assertFalse(x)
bool(x) is False
assertIs(a, b)
a is b
3.1
assertIsNot(a, b)
a is not b
3.1
assertIsNone(x)
x is None
3.1
assertIsNotNone(x)
x is not None
3.1
assertIn(a, b)
a in b
3.1
assertNotIn(a, b)
a not in b
3.1
assertIsInstance(a, b)
isinstance(a, b)
3.2
assertNotIsInstance(a, b)
not isinstance(a, b)
3.2
assertGreater(a, b)
a > b
3.1
assertGreaterEqual(a, b)
a >= b
3.1
assertLess(a, b)
a < b
3.1
assertLessEqual(a, b)
a <= b
3.1
测试夹具 (FixTure) 测试夹具表示执行一项或多项测试夹具 所需的准备工作 测试,以及任何关联的清理操作。例如,这可能涉及 创建临时或代理数据库、目录或启动服务器 过程。
setup() 用例前置方法,每执行一个测试用例 (def) 之前都会调用一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import unittestclass TestDemo (unittest.TestCase): def setUp (self ) -> None : print ("def init" ) def test_01_login (self ) -> None : print ("login case" ) def test_02_logout (self ) -> None : print ("logout case" ) if __name__ == '__main__' : unittest.main()
TearDown() 用例后置方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import unittestclass TestDemo (unittest.TestCase): def setUp (self ) -> None : print ("def setup" ) def tearDown (self ) -> None : print ("def tearDown" ) def test_01_login (self ) -> None : print ("login case" ) def test_02_logout (self ) -> None : print ("logout case" ) if __name__ == '__main__' : unittest.main()
setUpClass() 类前置方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import unittestclass TestDemo (unittest.TestCase): def setUpClass (cls ) -> None : print ("class setUp" ) def setUp (self ) -> None : print ("def setup" ) def tearDown (self ) -> None : print ("def tearDown" ) def test_01_login (self ) -> None : print ("login case" ) def test_02_logout (self ) -> None : print ("logout case" ) if __name__ == '__main__' : unittest.main()
TearDownClass() 类后置方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import unittestclass TestDemo (unittest.TestCase): def setUpClass (cls ) -> None : print ("class setUp" ) def tearDownClass (cls ) -> None : print ("class tearDown" ) def setUp (self ) -> None : print ("def setup" ) def tearDown (self ) -> None : print ("def tearDown" ) def test_01_login (self ) -> None : print ("login case" ) def test_02_logout (self ) -> None : print ("logout case" ) if __name__ == '__main__' : unittest.main()
测试套件 (TestSuite) 测试套件 是测试用例和/或测试套件的集合。收集测试用例
在正常的命令执行时,一次只能运行一个文件或一个测试用例;使用测试套件就可以将一些python文件放在一起执行
创建测试套件 1 2 3 4 import unittestsuite = unittest.TestSuite()
创建用例加载器 - 装载器 1 2 3 4 5 6 7 8 import unittestsuite = unittest.TestSuite() loader = unittest.TestLoader()
添加用例
loadTestsFromTestCase(‘测试用例的类名’) - 通过测试类对象加载其中的所有测试函数,生成测试套件
loadTestsFromModule(‘测试用例文件名/模块名’, , pattern=’test_*.py’) - 通过测试模块加载其中的所有测试用例,生成测试套件
loadTestsFromName(‘测试用例文件名/测试用例类名通用’) - 通过字符串格式的测试函数导入路径名
loadTestsFromNames(names, module=None):通过测试函数导入路径名,批量加载测试用例
getTestCaseNames(testCaseClass):通过测试类获取其中所有测试函数的测试函数导入路径名
discover(start_dir=’./‘, pattern=’test_*.py’) 模块过滤 装载器
start_dir筛选路径, pattern筛选文件名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import unittestsuite = unittest.TestSuite() loader = unittest.TestLoader() from demo1.test01Demo import TestDemotestClass01 = loader.loadTestsFromTestCase(TestDemo) from demo1 import test01DemotestClass02 = loader.loadTestsFromModule(TestDemo) testClass03 = loader.discover(r"C:\test\testCase\", pattern=" test_*.py") suite.addTest(TestClass01()) suite.addTest(TestClass02()) suite.addTest(TestClass03()) # or suite.addTests([ TestClass01(), TestClass02(), TestClass03() ])
获取套件中的用例数量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import unittestsuite = unittest.TestSuite() loader = unittest.TestLoader() ''' 其他: suite 属性 _testMethodName:字符串类型,测试方法(函数)名 _testMethodDoc:字符串类型,测试方法(函数)docstring注释 _outcome: 用例结果,unittest._Outcome对象,包含测试结果result(TestResult对象)及用例过执行异常errors等 id():用例标识,为用例的导入路径,例如test_demo.TestDemo.test_a shortDescription():用例描述,_testMethodDoc的第一行内容 defaultTestResult():默认使用的测试结果result对象(新建一个TestResult对象) countTestCases():用例数量,固定为1 run(result=None):用例运行方法 debug():用例调试方法,不纪录测试结果 fail(msg=None):使用例失败 skipTest(reason):使用例跳过(抛出SkipTest异常) for test in suite: print('用例id:', test.id()) print('用例描述:', test.shortDescription()) print('测试方法(函数)名:', test._testMethodName) print('测试方法(函数)完整docstring:', test._testMethodDoc) print('所属测试类(名称):', test.__class__.__name__) # 可以通过 用例 测试类对象 + 对应的测试方法名 获取测试方法对象 testMethod = getattr(test.__class__, test._testMethodName) print('测试方法(函数)对象:', testMethod) # 通过测试方法对象可以拿到很多相关信息 print('测试方法(函数)名:', testMethod.__name__) # 同test._testMethodName print('测试方法(函数)完整docstring:', testMethod.__doc__) # 同test._testMethodDoc ''' caseNumber = suite.countTestCases() print (caseNumber)
执行测试套件 unittest的TestResult 类方法 常用方法
wasSuccessful():是否全部成功
stop():停止当前测试运行
startTest(test):开始(纪录)测试某用例
stopTest(test):停止(纪录)测试某用例
startTestRun():开始(纪录)整体的测试运行
stopTestRun():停止(纪录)整体的测试运行
addError(test, err):纪录异常用例
addFailure(test, err):纪录失败的用例
addSuccess(test):纪录成功的用例(默认什么都不做)
addSkip(test, reason):纪录跳过的测试用例
addExpectedFailure(test, err):纪录期望失败的测试用例
addUnexpectedSuccess(test):纪录非预期成功的测试用例
addSubTest(test, subtest, outcome):纪录子测试
unittest - 用例加载器&执行测试套件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import unittestsuite = unittest.TestSuite() loader = unittest.TestLoader() ''' ... 添加用例逻辑 ''' ''' TextTestRunner() __init__ 方法参数: stream = f f为文件流,测试结果将会写入到该文件流中 verbosity = 0 默认值为0,默认输出; 2 详细输出(测试类,测试用例,测试结果) ''' runner = unittest.TextTestRunner() runner.run(suite)
直接传入路径加载放入到测试套件中 1 2 3 4 5 6 7 8 9 10 import unittestsuite = unittest.TestSuite() suite = unittest.defaultTestLoader.discover(r'C:\test\testCase\') runner = unittest.TextTestRunner() runner.run(suite)
使用第三方库 BeautfulReport 生成html报告 1 2 3 4 5 6 7 8 9 10 import unittestimport BeautfulReport as HtmlReportsuite = unittest.TestSuite() suite = unittest.defaultTestLoader.discover(r'C:\test\testCase\') runner = HtmlReport(suites=suite) runner.report(description="测试报告描述")
进阶 装饰器
@unittest.expectedFailure
测试用例结果为失败时,则pass
@unittest.skip(‘跳过的原因’)
无条件跳过装饰测试。
@unittest.skipIf(‘跳过条件’, ‘跳过的原因’)
如果条件 为 true,则跳过修饰测试
@unittest.skipUnless(‘不跳过的条件’, ‘不跳过的原因’)
条件为 false,则跳过修饰测试
测试覆盖率 coverage.py
是一个用于评估Python代码测试覆盖率的工具。它可以帮助你确定你的测试用例是否足够全面地覆盖了代码,并且可以生成报告来帮助你识别哪些代码没有被测试到。
使用coverage.py
很简单,只需要在运行测试之前安装它,然后在测试运行时使用coverage run
命令来运行测试。
安装coverage
执行
1 coverage run test_divi.py
生成文件
htmlcov 数据生成html
数据驱动测试DDT 数据驱动测试的基本概念、引读
当我们进行测试时遇到执行步骤相同,只需要改变入口参数的测试时,使用DDT可以简化代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class BasicTestCase (unittest.TestCase): def test1 (self, num1 ): num = num1 + 1 print ('number:' , num) def test2 (self, num2 ): num = num2 + 1 print ('number:' , num) def test3 (self, num3 ): num = num3 + 1 print ('number:' , num) @ddt class BasicTestCase (unittest.TestCase): @data('666' , '777' , '888' ) def test (self, num ): print ('数据驱动的number:' , num)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ddt class BasicTestCase (unittest.TestCase): @data('666' , '777' , '888' ) def test (self, num ): print ('数据驱动的number:' , num)
1 2 3 4 5 6 7 8 9 10 11 @ddt class BasicTestCase (unittest.TestCase): @data(['张三' , '18' ], ['李四' , '19' ] ) @unpack def test (self, name, age ): print ('姓名:' , name, '年龄:' , age)
txt格式文件驱动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def read_num (): lis = [] with open ('num' , 'r' , encoding='utf-8' ) as file: for line in file.readlines(): lis.append(line.strip('\n' )) return lis @ddt class BasicTestCase (unittest.TestCase): @data(*read_num( ) ) def test (self, num ): print ('数据驱动的number:' , num)
多参数数据驱动
读取函数中的数据分割、@unpack解包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def read_dict (): lis = [] with open ('dict' , 'r' , encoding='utf-8' ) as file: for line in file.readlines(): lis.append(line.strip('\n' ).split(',' )) return lis @ddt class BasicTestCase (unittest.TestCase): @data(*read_dict( ) ) @unpack def test (self, name, age ): print ('姓名为:' , name, '年龄为:' , age)
json格式文件驱动
1 2 3 4 5 6 7 8 9 10 11 12 def read_num_json (): return json.load(open ('num.json' , 'r' , encoding='utf-8' )) @ddt class BasicTestCase (unittest.TestCase): @data(*read_num_json( ) ) def test (self, num ): print ('读取的数字是' , num)
多参数数据驱动(以列表形式存储多参数)
@unpack装饰器的添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def read_dict_json (): return json.load(open ('dict.json' , 'r' , encoding='utf-8' )) @ddt class BasicTestCase (unittest.TestCase): @data(*read_dict_json( ) ) @unpack def test (self, name, age ): print ('姓名:' , name, '年龄:' , age)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def read_dict_json (): return json.load(open ('dictx.json' , 'r' , encoding='utf-8' )) @ddt class BasicTestCase (unittest.TestCase): @data(*read_dict_json( ) ) @unpack def test (self, name, age ): print ('姓名:' , name, '年龄:' , age)
yaml格式文件驱动 在unittest测试框架中,对yaml数据格式的支持十分强大,使用非常方便
yaml文件的数据驱动执行代码十分简单!!!(但是要注意细节)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ddt class BasicTestCase (unittest.TestCase): @file_data('num.yml' ) def test (self, num ): print ('读取的数字是' , num)
多参数数据驱动
形参入口和数据参数key命名统一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ddt class BasicTestCase (unittest.TestCase): @file_data('dict.yml' ) def test (self, name, age ): print ('姓名是:' , name, '年龄为:' , age)
特殊情况:当入口与文件中数据参数无法统一命名时,解决办法
1 2 3 4 5 6 @ddt class BasicTestCase (unittest.TestCase): @file_data('dict.yml' ) def test (self, **cdata ): print ('姓名是:' , cdata['name' ], '年龄为:' , cdata['age' ])
案例 开发一个接口测试框架
需求:
支持用例优先级、标签、支持通过优先级或标签帅选用例
支持用例负责人、迭代,以及通过负责人或迭代人筛选用例
支持多环境配置
支持超时重试机制,防止不稳定用例
并发执行用例以提高用例回归效率
为用例添加额外属性
支持用例优先级、标签、支持通过优先级或标签帅选用例
支持用例负责人、迭代,以及通过负责人或迭代人筛选用例
实现的步骤为:
编写用例时,在测试函数上添加特殊标记
正常加载用例生成测试套件
遍历测试套件所有测试用例,根据条件(如优先级为P0),和用例特殊标记 筛选生成新的测试套件
方法一:测试函数注释,如在测试函数注释第一行以外添加特定格式字符串如priority:0等
方法二:测试函数对象,为测试函数对象添加额外属性,可以通过装饰器实现
方法三:测试函数名,如test_a_p0,多种标签可能导致用例名太长或标识度不高 (不推荐)
通过测试函数注释-添加用例优先级属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import reimport unittestdef get_suite_tests (suite ): tests = [] for test in suite: if isinstance (test, unittest.TestCase): tests.append(test) else : tests.extend(get_suite_tests(test)) return tests class TestDemo (unittest.TestCase): def test_a (self ): """测试a priority:0 """ def test_b (self ): """测试b priority:1 """ def test_c (self ): """测试c priority:1 """ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo) new_suite = unittest.TestSuite() for test in get_suite_tests(suite): matched = re.search(r'priority:(\d)' , test._testMethodDoc) if matched: priority = matched.group(1 ) else : priority = None print (f"用例:{test._testMethodName} 优先级: {priority} " ) if priority == 0 : new_suite.addTest(test) print ('筛选得到的用例数量:' , new_suite.countTestCases())
通过测试函数对象-添加额外属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import unittestdef get_suite_tests (suite ): tests = [] for test in suite: if isinstance (test, unittest.TestCase): tests.append(test) else : tests.extend(get_suite_tests(test)) return tests def test (priority=None , tags=None , owner=None , iteration=None ): """装饰器,为测试函数添加额外属性""" def decorator (func ): func.priority = priority func.tags = tags func.owner = owner func.iteration = iteration return func return decorator class TestDemo (unittest.TestCase): @test(priority=0 , tags=['demo' ], owner='superhin' , iteration='v1.0.0' ) def test_a (self ): """测试a""" @test(priority=1 , tags=['demo' ], owner='superhin' , iteration='v1.0.0' ) def test_b (self ): """测试b""" @test(priority=1 , tags=['demo' ], owner='superhin' , iteration='v2.0.0' ) def test_c (self ): """测试c""" suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo) new_suite = unittest.TestSuite() for test in get_suite_tests(suite): testMethod = getattr (test.__class__, test._testMethodName) if hasattr (testMethod, 'priority' ) and getattr (testMethod, 'priority' ) == 0 : new_suite.addTest(test) print ('筛选得到的用例数量:' , new_suite.countTestCases())
通过测试类-类属性设置用例通用属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import unittestdef get_suite_tests (suite ): tests = [] for test in suite: if isinstance (test, unittest.TestCase): tests.append(test) else : tests.extend(get_suite_tests(test)) return tests def safe_getattr (obj, attr ): """对象没有该属性时返回None""" if hasattr (obj, attr): return getattr (obj, attr) def test (priority=None , tags=None , owner=None , iteration=None ): """装饰器,为测试函数添加额外属性""" def decorator (func ): func.priority = priority func.tags = tags func.owner = owner func.iteration = iteration return func return decorator class TestCase (unittest.TestCase): def __init__ (self, methodName='runTest' ): super ().__init__(methodName) testMethod = getattr (self, methodName) testClass = self.__class__ self.priority = safe_getattr(testMethod, 'priority' ) or safe_getattr(testClass, 'priority' ) self.tags = safe_getattr(testMethod, 'tags' ) or safe_getattr(testClass, 'tags' ) self.owner = safe_getattr(testMethod, 'owner' ) or safe_getattr(testClass, 'owner' ) self.iteration = safe_getattr(testMethod, 'iteration' ) or safe_getattr(testClass, 'iteration' ) class TestDemo (TestCase ): owner = 'superhin' iteration = 'v1.0.0' tags = ['demo' ] @test(priority=0 ) def test_a (self ): """测试a""" @test(priority=1 ) def test_b (self ): """测试b""" @test(priority=1 , iteration='v2.0.0' ) def test_c (self ): """测试c""" suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo) new_suite = unittest.TestSuite() for test in get_suite_tests(suite): print ("用例适用版本:" , safe_getattr(test, 'iteration' )) if safe_getattr(test, 'iteration' ) == 'v1.0.0' : new_suite.addTest(test) print ('筛选得到的用例数量:' , new_suite.countTestCases())
测试计划-通过属性筛选测试用例 增加一个自定义的TestPlan对象,通过类属性描述
1 2 3 4 5 6 7 8 9 10 11 class TestPlanDemo1 (TestPlan ): test_dir = '../testcases' filter = { "priorities" : [0 , 1 ], "tags" : ["demo" ], } if __name__ == '__main__' : TestPlanDemo1().run(verbosity=2 )
用例过滤函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def filter_by_priorities (tests, priorities ): """通过优先级列表筛选用例,返回筛选后用例对象列表""" new_tests = [] for test in tests: if safe_getattr(test, 'priority' ) in priorities: new_tests.append(test) return new_tests def filter_by_tags (tests, tags ): """通过tags筛选用例,返回筛选后用例对象列表""" new_tests = [] for test in tests: test_tags = safe_getattr(test, 'tags' ) or [] print (tags, test_tags) if set (tags) - set (test_tags) == set (): new_tests.append(test) return new_tests
测试计划类实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class TestPlan : test_dir: str = None tests = [] filter : dict = None def __init__ (self ): loader = unittest.defaultTestLoader if self.tests: suite = loader.loadTestsFromNames(self.tests) elif self.test_dir: print ('self.test_dir' , self.test_dir) suite = loader.discover(start_dir=self.test_dir) else : raise ValueError("测试计划必须包含start_dir或tests属性" ) if self.filter : tests = get_suite_tests(suite) if 'priorities' in self.filter : tests = filter_by_priorities(tests, self.filter ['priorities' ]) if 'tags' in self.filter : tests = filter_by_tags(tests, self.filter ['tags' ]) suite = unittest.TestSuite() suite.addTests(tests) self.suite = suite def run (self, verbosity=1 ): runner = unittest.TextTestRunner(verbosity=verbosity) runner.run(self.suite)