python之UnittTest模块
一. UnitTest单元测试框架
1.1 unittest概述
unittest原名为PyUnit,是由java的JUnit衍生而来。单元测试是对程序中最小的可测试模块(函数)来进行测试;对于单元测试,需要设置预先条件,对比预期结果和实际结果。
unittest有四个重要的面向对象概念:
1)test fixture:这个概念主要处理测试环境的搭建和清理。很多时候我们在进行测试的时候需要搭建合适的环境,例如创建目录、创建数据库等,而在测试完毕后这些环境又不再需要了。test fixture可以帮我们很好的处理这些事情。
2)test case: 既然要进行测试,测试用例当然是最重要的,每一项测试内容都是一个test case。测试用例是指一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。单元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
3)test suite:我们当然不希望只能一项项的进行测试,最好是将要测试的项目放在一起。test suite相当于test case的集合,当然test suite也能嵌套在test suite中。
4)test runner:顾名思义,这个概念负责执行测试并控制结果输出。
官方文档网址:https://doc.codingdict.com/python_352/library/unittest.html
1.2 unittest的属性和子类
<1>unittest的属性如下:
['BaseTestSuite', 'FunctionTestCase', 'SkipTest', 'TestCase', 'TestLoader', 'TestProgram', 'TestResult', 'TestSuite', 'TextTestResult', 'TextTestRunner', '_TextTestResult','__all__', '__builtins__',
'__doc__', '__file__', '__name__', '__package__', '__path__', '__unittest','case', 'defaultTestLoader', 'expectedFailure', 'findTestCases', 'getTestCaseNames', 'installHandler', 'loader', 'main',
'makeSuite', 'registerResult', 'removeHandler', 'removeResult', 'result', 'runner', 'signals', 'skip', 'skipIf', 'skipUnless', 'suite', 'util']
unittest类常用属性和方法说明:
1)unittest.TestCase: TestCase类,所有测试用例类继承的基本类。用法:class BaiduTest(unittest.TestCase):
2)unittest.main(): 使用它可以方便的将一个单元测试模块变为可直接运行的测试脚本,main()方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并自动执行他们。执行方法的默认顺序是:根据ASCII码的顺序加载测试用例,数字与字母的顺序为: 0-9,A-Z,a-z。所以以A开头的测试用例方法会优先执行,以a开头会后执行。
3)unittest.TestSuite(): unittest框架的TestSuite()类是用来创建测试套件的。
4)unittest.TextTestRunner(): unittest框架的TextTestRunner()类,通过该类下面的run()方法来运行suite所组装的测试用例,入参为suite测试套件。
5)unittest.defaultTestLoader(): defaultTestLoader()类,通过该类下面的discover()方法可自动根据测试目录start_dir匹配查找测试用例文件(test*.py),并将查找到的测试用例组装到测试套件,因此可以直接通过run()方法执行discover。用法如下:
discover=unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
6)unittest.skip(): 装饰器,当运行用例时,有些用例可能不想执行,可用装饰器暂时屏蔽该条测试用例。一种常见的用法:想调试某一个测试用例,而先屏蔽其他用例时就可以用装饰器屏蔽。
@unittest.skip(reason): skip(reason)装饰器: 无条件跳过装饰的测试,并说明跳过测试的原因。
@unittest.skipIf(reason): skipIf(condition,reason)装饰器: 条件为真时,跳过装饰的测试,并说明跳过测试的原因。
@unittest.skipUnless(reason): skipUnless(condition,reason)装饰器:条件为假时,跳过装饰的测试,并说明跳过测试的原因。
@unittest.expectedFailure(): expectedFailure()测试标记为失败。
<2>unittest的TestCase类的属性如下:
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__','__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_addSkip', '_baseAssertEqual', '_classSetupFailed', '_deprecate', '_diffThreshold', '_formatMessage',
'_getAssertEqualityFunc','_truncateMessage', 'addCleanup', 'addTypeEqualityFunc', 'assertAlmostEqual', 'assertAlmostEquals', 'assertDictContainsSubset', 'assertDictEqual', 'assertEqual',
'assertEquals', 'assertFalse', 'assertGreater','assertGreaterEqual', 'assertIn', 'assertIs', 'assertIsInstance', 'assertIsNone', 'assertIsNot', 'assertIsNotNone', 'assertItemsEqual', 'assertLess',
'assertLessEqual', 'assertListEqual', 'assertMultiLineEqual','assertNotAlmostEqual', 'assertNotAlmostEquals', 'assertNotEqual', 'assertNotEquals', 'assertNotIn', 'assertNotIsInstance',
'assertNotRegexpMatches', 'assertRaises', 'assertRaisesRegexp', 'assertRegexpMatches','assertSequenceEqual', 'assertSetEqual', 'assertTrue', 'assertTupleEqual', 'assert_', 'countTestCases', 'debug',
'defaultTestResult', 'doCleanups', 'fail', 'failIf', 'failIfAlmostEqual', 'failIfEqual','failUnless', 'failUnlessAlmostEqual', 'failUnlessEqual', 'failUnlessRaises', 'failureException', 'id','longMessage',
'maxDiff', 'run','setUp','setUpClass', 'shortDescription', 'skipTest', 'tearDown','tearDownClass']
TestCase类常用属性和方法说明:
1)setUp() 方法用于测试用例执行前的初始化工作。如测试用例中需要访问数据库,可以在setUp中建立数据库连接并进行初始化。如测试用例需要登录web,可以先实例化浏览器。在所有的测试方法调用之前调用(自动调用),用来测试fixture,除了AssertionError或SkipTest之外,该方法抛出的异常都视为error,而不是测试不通过。
2)tearDown() 方法用于测试用例执行之后的善后工作。如关闭数据库连接。关闭浏览器。tearDown() 清理函数,在所有的测试方法调用之后调用(自动调用),无参数,无返回值。测试方法抛出异常,该方法也正常调用,该方法抛出的异常都视为error,而不是测试不通过。只有setUp()调用成功,该方法才会被调用。没有默认的实现。通过setup 和 tearDown组装一个module成为一个固定的测试装置。注意:如果setup运行抛出错误,测试用例代码不会执行。但是,如果setup执行成功,不管测试用例是否执行成功都会执行teardown。
3)setUpClass() 类初始化方法,在单个类中的所有测试方法调用之前调用,setUpClass作为唯一的参数被调用时,必须使用classmethod()作为装饰器。
4)tearDownClass() 类清理方法,在单个类中的所有测试方法调用之后调用,tearDownClass作为唯一的参数被类调用,必须使用classmethod()作为装饰器。

import unittest class Test(unittest.TestCase): @classmethod def setUpClass(cls): #这里的cls是当前类的对象 cls._connection = createExpensiveConnectionObject() @classmethod def tearDownClass(cls): cls._connection.destroy()
注意: 用setUp与setUpClass、teardown 和tearDownClass的区别
setup():每个测试case运行前运行
teardown():每个测试case运行完后执行
setUpClass():必须使用@classmethod 装饰器,有case运行前只运行一次
tearDownClass():必须使用@classmethod装饰器,所有case运行完后只运行一次
5)run(result=None)运行测试,将测试结果传递到TestResult对象中,如果结果被省略或者没有,则创建一个临时结果对象(通过调用defaultTestResult()方法)并使用。
6)skipTest(reason):在测试方法或setUp调用该方法可跳过当前测试
7)debug():以不采集测试结果方式运行测试
8)shortDescription():返回一行描述的测试结果信息
9)assert*() 一些断言方法:在执行测试用例的过程中,最终用例是否执行通过,是通过判断测试得到的实际结果和预期结果是否相等决定的。
TestCase类提供的常用断言方法总结如下:

Methed #基本断言 Checks that New in python 1.assertEqual(first, second, msg=None) a == b 2.assertNotEqual(first, second, msg=None) a != b 3.assertTrue(expr, msg=None) bool(expr) is True 4.assertFalse(expr, msg=None) bool(expr) is False 5.assertIsNot(a,b, msg=None) a is not b 3.1 6.assertIs(a,b, msg=None) a is b 3.1 7.assertIsNone(x, msg=None) x is None 3.1 8.assertIsNotNone(x, msg=None) x is Not None 3.1 9.assertIn(a,b, msg=None) a in b 3.1 10.assertNotIn(a,b, msg=None) a not in b 3.1 11.assertIsInstance(a,b, msg=None) isinstance(a,b) 3.2 12.assertNotIsInstance(a,b, msg=None) not isinstance(a,b) 3.2 13.assertRaises(exception, callable, *args, **kwds) fun(*args, **kwds) raises exc 14.assertRaisesRegex(exception, regex, callable, *args, **kwds) fun(*args, **kwds) raises exc and the message matches regex r 3.1 15.assertWarns(warn, fun, *args, **kwds) fun(*args, **kwds) raises warn 3.2 16.assertWarnsRegex(warn, r, fun, *args, **kwds) fun(*args, **kwds) raises warn and the message matches regex r 3.2 17.assertLogs(logger, level) 一个上下文管理器,用于测试至少一条消息是否记录在logger或其中一个子项上,至少包含给定的级别。 3.4 #比较断言 1.assertAlmostEqual(a, b, places=7, msg=None, delta=None) 计算差值a-b后四舍五入到给定的小数点后places指定的十进制位(默认值7),再与零比较。相当于round(a-b, 7) == 0 #在上述函数中,如果delta指定了值,则a和b之间的差值必须≤delta 2.assertNotAlmostEqual(a, b, places=7, msg=None, delta=None) round(a-b, 7) != 0 #在上述函数中,如果delta指定了值,则a和b之间的差值必须 >delta 3.assertGreater(a, b, msg=None) a > b 3.1 4.assertGreaterEqual(first, second, msg=None) a >= b 3.1 5.assertLess(a, b, msg=None) a < b 3.1 6.assertLessEqual(a, b, msg=None) a <= b 3.1 7.assertRegex(text, regex, msg=None) r.search(text) 3.2 8.assertNotRegex(text, regex, msg=None not r.search(text) 3.2 #测试regex搜索是否与文本匹配(或不匹配)。如果失败,错误消息将包括模式和文本(或模式和文本的意外匹配的部分)。regex可以是正则表达式对象或包含适用于re.search()的正则表达式的字符串。 9.assertCountEqual(first, second, msg=None) 测试序列1包含序列2相同的元素,不需要考虑元素的顺序。 #注意当序列1、2元素不同时,将生成列出序列之间差异的错误消息。该方法不会忽略重复元素,他比较的是每个元素的出现次数是否相等,类似于assertEqual(Counter(list(first)), Counter(list(second))) 10.assertMultiLineEqual(a, b, msg=None) 多行字符串 a = b 11.assertSequenceEqual(first, second, msg=None, seq_type=None) 测试两个序列是否相等。如果提供seq_type,则第一和第二必须是seq_type的实例,否则将引发失败。如果序列不同,则构造示出两者之间的差异的错误消息。 12.assertListEqual(first, second, msg=None) 13.assertTupleEqual(first, second, msg=None) #测试两个列表或元组是否相等。如果没有,则构造一个错误消息,仅显示两者之间的差异。如果任一参数的类型错误,也会引发错误。在将列表或元组用assertEqual()进行比较时,默认使用这些方法。 14.assertSetEqual(a, b, msg=None) 测试两集合相等。如果没有,则构造一个错误消息,列出集合之间的差异。 15.assertDictEqual(first, second, msg=None) 测试两个字典是否相等。如果没有,则构造一个错误消息,显示字典中的差异。
<3>unittest的TestSuite类的属性如下:(组织用例时需要用到)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__','__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_addClassOrModuleLevelException',
'_get_previous_module', '_handleClassSetUp', '_handleModuleFixture', '_handleModuleTearDown','_tearDownPreviousClass', '_tests', 'addTest', 'addTests', 'countTestCases',
'debug', 'run']
TestSuite类常用属性和方法总结:
1)addTest(test): 该方法是将测试用例test添加到测试套件中。
示例:将test_baidu模块下的BaiduTest类下的test_baidu测试用例添加到测试套件。
suite = unittest.TestSuite()
suite.addTest(test_baidu.BaiduTest('test_baidu'))
2) addTests(tests): 该方法用于将多个测试用例添加到测试套件中。
3)run(result): 运行与此套件关联的测试,将结果收集到作为result传递的测试结果对象中。注意,不同于TestCase.run(),TestSuite.run()需要传入结果对象。
4)countTestCases(): 返回此测试对象表示的测试数,包括所有单个测试和子套件。
TestSuite类使用综合示例:

import unittest from testCase.mathfunctest import * #构建测试类 #coding = 'utf-8' class Count(object): def add(self,x,y): return x+y def sub(self,x,y): return x-y def multi(self,a, b): return a * b def divide(self,a, b): return a / b #定义测试类,父类为unittest.TestCase。 class TestCount(unittest.TestCase): #定义setUp()方法用于测试用例执行前的初始化工作 @classmethod def setUpClass(cls): print('setUp') cls.obj = Count() #定义多个测试用例,以“test_”开头命名的方法 def test_add(self): self.assertEqual(15, self.obj.add(10, 5)) def test_sub(self): self.assertEqual(5, self.obj.sub(10, 5)) def test_multi(self): self.assertEqual(50, self.obj.multi(10, 5)) def test_divide(self): self.assertEqual(2, self.obj.divide(10, 5)) self.assertEqual(2.5,self.obj.divide(5, 2)) # 定义tearDown()方法用于测试用例执行之后的善后工作。 @classmethod def tearDownClass(cls): print('tearDown') cls.obj = None #构建测试集示例1: def get_suite1(): #使用addTest testcase1 = TestCount('test_add') suite = unittest.TestSuite() suite.addTest(testcase1) return suite def get_suite2(): #使用addTests case_list = ['test_add','test_sub','test_multi','test_divide'] demos = map(TestCount,case_list) suite = unittest.TestSuite() suite.addTests(demos) return suite if __name__ == '__main__': s = get_suite2() print(s) #<unittest.suite.TestSuite tests=[<__main__.TestCount testMethod=test_add>, <__main__.TestCount testMethod=test_sub>, # <__main__.TestCount testMethod=test_multi>, <__main__.TestCount testMethod=test_divide>]> print(s.countTestCases())#计算测试用例数 #实例化一个result对象 result = unittest.TestResult() test_result = s.run(result) print(test_result)#<unittest.result.TestResult run=4 errors=0 failures=0>
<4>unittest的TestLoader类的属性如下
['errors', '_loading_packages', '__module__', '__doc__', 'testMethodPrefix', 'sortTestMethodsUsing', 'suiteClass', '_top_level_dir', '__init__', 'loadTestsFromTestCase',
'loadTestsFromModule', 'loadTestsFromName','loadTestsFromNames', 'getTestCaseNames', 'discover', '_get_directory_containing_module', '_get_name_from_path',
'_get_module_from_name', '_match_path', '_find_tests', '_find_test_path', '__dict__', '__weakref__','__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__',
'__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__','__init_subclass__', '__format__',
'__sizeof__', '__dir__', '__class__']
TestLoader类常用属性、方法说明:
1)loadTestsFromTestCase(testCaseClass):返回一组包含在TestCase 派生类 TestCaseClass中的所有测试用例构成的测试套件。
2)loadTestsFromModule(module, pattern=None):返回一组包含在给定模块中的所有测试用例构成的TestSuite。此方法在模块中搜索从TestCase派生的类,并为类定义的每个测试方法创建类的实例。
3)loadTestsFromName(name, module=None):从字符串说明符中返回一组测试用例,说明符name是可以解析为模块,测试用例类,测试用例类中的测试方法
4)loadTestsFromNames(names, module=None):类似于loadTestsFromName(),但是接受一系列名称而不是单个名称。返回值是一个测试套件。
5)getTestCaseNames(testCaseClass)返回在testCaseClass中找到的方法名称的排序序列,返回形式是列表;testCaseClass应该是TestCase的子类。
6)discover(start_dir, pattern='test*.py', top_level_dir=None)通过从指定的起始目录递归到子目录中找到所有测试模块,并返回包含它们的TestSuite对象。仅加载与模式匹配的测试文件。
7)属性1:testMethodPrefix 返回字符串,返回将被解释为测试方法的方法名称的前缀。默认值为'test'。这会影响getTestCaseNames()和所有loadTestsFrom*()方法。
8)属性2:sortTestMethodsUsing用于在getTestCaseNames()和所有loadTestsFrom*()方法中对方法名称进行排序时用于比较方法名称的函数。
9)属性3:suiteClass从测试列表构造测试套件的可调用对象
TestLoader类使用方法的综合示例:
步骤1:先在mathfunc.py中准备待测类

#构建待测试类 #coding = 'utf-8' class Count(object): def add(self,x,y): return x+y def sub(self,x,y): return x-y def multi(self,a, b): return a * b def divide(self,a, b): return a / b
步骤2:在test_mathfunc.py中定义测试类,测试类中添加测试方法

import unittest from testCase.mathfunc import * #定义测试类,父类为unittest.TestCase。 class TestCount(unittest.TestCase): #定义setUp()方法用于测试用例执行前的初始化工作 @classmethod def setUpClass(cls): print('setUp') cls.obj = Count() #定义多个测试用例,以“test_”开头命名的方法 def test_add(self): self.assertEqual(15, self.obj.add(10, 5)) def test_sub(self): self.assertEqual(5, self.obj.sub(10, 5)) def test_multi(self): self.assertEqual(50, self.obj.multi(10, 5)) def test_divide(self): self.assertEqual(2, self.obj.divide(10, 5)) self.assertEqual(2.5,self.obj.divide(5, 2)) # 定义tearDown()方法用于测试用例执行之后的善后工作。 @classmethod def tearDownClass(cls): print('tearDown') cls.obj = None
步骤3:为了演示testLoader跨模块加载测试用例,在test_string.py中定义其他测试类,并添加测试方法

import unittest class TestStringMethods(unittest.TestCase): def test_upper(self): self.assertEqual('foo'.upper(), 'FOO') def test_isupper(self): self.assertTrue('FOO'.isupper()) self.assertFalse('Foo'.isupper()) def test_split(self): s = 'hello world' self.assertEqual(s.split(), ['hello', 'world']) # check that s.split fails when the separator is not a string with self.assertRaises(TypeError): s.split(2)
步骤4:在main.py中展示test_Loader构建测试用例套件的方法及

#coding = 'utf-8' import unittest import os from test_math_func import * #构建测试集示例1: def get_suite1(): #使用TestSuite()类方法addTest单个加载测试用例 testcase1 = TestCount('test_add') suite = unittest.TestSuite() suite.addTest(testcase1) return suite def get_suite2(): #使用TestSuite()类方法addTests同时加载多个测试用例 case_list = ['test_add','test_sub','test_multi','test_divide'] demos = map(TestCount,case_list) suite = unittest.TestSuite() suite.addTests(demos) return suite def get_suite3(): #TestLoder类的基本使用详解 #法1.从testCase派生的测试类下加载该类下的所有测试用例,使用方法loadTestsFromTestCase(testCaseClass) test_loader = unittest.TestLoader() suite = test_loader.loadTestsFromTestCase(TestCount) #print(suite)#<unittest.suite.TestSuite tests=[<__main__.TestCount testMethod=test_add>, <__main__.TestCount testMethod=test_divide>, <__main__.TestCount testMethod=test_multi>, <__main__.TestCount testMethod=test_sub>]> #法2.加载给定模块中的所有测试用例,使用方法loadTestsFromModule(Module) #先导入待加载测试用例的模块 import test_math_func suite1 = test_loader.loadTestsFromModule(test_math_func) #法3.从字符串说明符中返回一组测试用例,使用方法loadTestsFromName(name, module=None)。说明符name是可以解析为模块,测试用例类,测试用例类中的测试方法 suite2 = test_loader.loadTestsFromName(name='test_math_func')#name解析为一个模块 #print(suite2)##<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_math_func.TestCount testMethod=test_add>, <test_math_func.TestCount testMethod=test_divide>, <test_math_func.TestCount testMethod=test_multi>, <test_math_func.TestCount testMethod=test_sub>]>]> suite3 = test_loader.loadTestsFromName(name='test_math_func.TestCount')#name解析为test_math_func模块下的测试用例类TestCount suite4 = test_loader.loadTestsFromName(name='test_math_func.TestCount.test_add') #print(suite4)#<unittest.suite.TestSuite tests=[<test_math_func.TestCount testMethod=test_add>]> #法4.从多个字符串说明图中返回一组测试用例,使用方法loadTestsFromNames(names, module=None) names = ('test_math_func','test_string')#多个模块名构成的元组 suite5 = test_loader.loadTestsFromNames(names=names) #print(suite5)#<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_math_func.TestCount testMethod=test_add>, <test_math_func.TestCount testMethod=test_divide>, <test_math_func.TestCount testMethod=test_multi>, <test_math_func.TestCount testMethod=test_sub>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_string.TestStringMethods testMethod=test_isupper>, <test_string.TestStringMethods testMethod=test_split>, <test_string.TestStringMethods testMethod=test_upper>]>]>]> #法5.getTestCaseNames(testCaseClass)返回在testCaseClass中找到的方法名称的排序序列,返回形式是列表;testCaseClass应该是TestCase的子类。 #print(test_loader.getTestCaseNames(test_math_func.TestCount))#['test_add', 'test_divide', 'test_multi', 'test_sub'] #法6.discover(start_dir, pattern='test*.py', top_level_dir=None)通过从指定的起始目录递归到子目录中找到所有测试模块,并返回包含它们的TestSuite对象。仅加载与模式匹配的测试文件。 start_dir = os.path.dirname(os.path.realpath(__file__)) suite6 = test_loader.discover(start_dir=start_dir,pattern='test_*.py') #print(suite6) #TestLoder类的属性 #TestLoader的以下属性可以通过实例化后赋值来配置 #属性1:testMethodPrefix 返回字符串,返回将被解释为测试方法的方法名称的前缀。默认值为'test'。这会影响getTestCaseNames()和所有loadTestsFrom*()方法。 print(test_loader.testMethodPrefix)#test #属性2:sortTestMethodsUsing用于在getTestCaseNames()和所有loadTestsFrom*()方法中对方法名称进行排序时用于比较方法名称的函数。 print(test_loader.sortTestMethodsUsing)#<function three_way_cmp at 0x0000022FEC5D6598> #属性3:suiteClass从测试列表构造测试套件的可调用对象 print(test_loader.suiteClass)#<class 'unittest.suite.TestSuite'> return suite6 if __name__ == '__main__': s = get_suite3() result = unittest.TestResult() result = s.run(result)
<5>unittest的TextTestRunner的属性如下
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', '_makeResult', 'buffer', 'descriptions', 'failfast', 'resultclass', 'run', 'stream', 'verbosity']
常用方法说明:
run(): run()方法是运行测试套件的测试用例,入参为suite测试套件。
runner = unittest.TextTestRunner()
runner.run(suite)
二. UnitTest的基本使用
2.1 UnitTest的基本使用方法
1)import unittest
2)定义一个继承自unittest.TestCase的测试用例类
3)定义setUp和tearDown,在每个测试用例前后做一些辅助工作。
4)定义测试用例,名字以test开头
5)一个测试用例应该只测试一个方面,测试目的和测试内容应很明确。主要是调用assertEqual、assertRaises等断言方法判断程序执行结果和预期值是否相符。
6)调用unittest.main()启动测试
7)如果测试未通过,会输出相应的错误提示。如果测试全部通过则不显示任何东西,这时可以添加-v参数显示详细信息
示例:
第一步:先在mathfunc.py中准备待测类:

#构建测试类 class Count(object): def add(self,x,y): return x+y def sub(self,x,y): return x-y def multi(self,a, b): return a * b def divide(self,a, b): return a / b
第二步:在test_mathfunc.py中定义测试类,测试类中添加测试方法

import unittest from mathfunc import * #定义测试类,父类为unittest.TestCase。 class TestCount(unittest.TestCase): #定义setUp()方法用于测试用例执行前的初始化工作 def setUp(self): print('setUp') self.obj = Count() #定义多个测试用例,以“test_”开头命名的方法 def test_add(self): self.assertEqual(15, self.obj.add(10, 5)) def test_sub(self): self.assertEqual(5, self.obj.sub(10, 5)) def test_multi(self): self.assertEqual(50, self.obj.multi(10, 5)) def test_divide(self): self.assertEqual(2, self.obj.divide(10, 5)) self.assertEqual(2.5,self.obj.divide(5, 2)) # 定义tearDown()方法用于测试用例执行之后的善后工作。 def tearDown(self): print('tearDown') self.obj = None
第三步:在test_suite.py中创建测试用例集,并运行测试用例集
创建测试用例集方法总结:
法1:逐个实例化测试用例,并通过addTest方法逐个添加测试用例到测试用例集

import unittest from test_mathfunc import * #(法1)逐个实例化测试用例,并通过addTest方法逐个添加测试用例到测试用例集 #定义测试用例管理函数 #构建测试集示例1: def get_suite(): #实例化测试用例 demo_countadd = TestCount('test_add') demo_countsub = TestCount('test_sub') demo_countmulti = TestCount('test_multi') demo_countdivide = TestCount('test_divide') #实例化测试用例集 suite = unittest.TestSuite() #通过addTest方法将测试用例加载到测试用例集 suite.addTest(demo_countadd) suite.addTest(demo_countsub) suite.addTest(demo_countmulti) suite.addTest(demo_countdivide) print(suite) return suite if __name__ == '__main__': s =get_suite() #统计测试用例条数 s.countTestCases() #实例化TextTestRunner类 runner = unittest.TextTestRunner() #使用run()方法运行测试套件(即运行测试套件中的所有用例) runner.run(s) #运行结果: #<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_sub>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_divide>]> #---------------------------------------------------------------------- #Ran 4 tests in 0.000s #OK
法2:通过map方法一次实例化多个测试用例,并通过addTests方法同时加载多条用例到测试用例集

import unittest from test_mathfunc import * #(法2)通过map方法一次实例化多个测试用例,并通过addTests方法同时加载多条用例到测试用例集 #构建测试集示例2: def get_suite(): case_list = ['test_add', 'test_sub','test_multi','test_divide'] #批量实例化测试用例 demos = map(TestCount, case_list) suite = unittest.TestSuite() #通过addTests方法批量加载测试用例 suite.addTests(demos) print(suite) return suite if __name__ == '__main__': s =get_suite() s.countTestCases() runner = unittest.TextTestRunner() runner.run(s) #运行结果: #<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_sub>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_divide>]> #---------------------------------------------------------------------- #Ran 4 tests in 0.000s #OK
法3:继承父类unittest.TestSuite,通过__init__方法直接批量添加测试用例

import unittest from test_mathfunc import * #(法3)继承父类unittest.TestSuite,通过__init__方法直接批量添加测试用例 #构建测试集示例3: class CountTestSuite(unittest.TestSuite): def __init__(self): case_list = ['test_add','test_sub','test_multi','test_divide'] unittest.TestSuite.__init__(self,map(TestCount,case_list)) if __name__ == '__main__': s = CountTestSuite() print(s) s.countTestCases() runner = unittest.TextTestRunner() runner.run(s) #运行结果 #<__main__.CountTestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_sub>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_divide>]> #---------------------------------------------------------------------- #Ran 4 tests in 0.000s #OK
法4:使用unittest.makeSuite()方法自动构建测试用例集,这种方法对于编写测试用例的要求:
(a)测试方法都以规定的命名开头
(b)使用makeSuite直接生成测试集
(c)unittest.makeSuite(testcaseClass,prefix='test') 说明:unittest.makeSuite()第一个参数是测试类,第二个参数定义加载的测试用例方法的开头字符

import unittest from test_mathfunc import * #(法4)使用unittest.makeSuite()方法自动构建测试用例集,这种方法对于编写测试用例的要求: #构建测试集示例4: def get_suite(): suite = unittest.makeSuite(TestCount,prefix='test') return suite if __name__ == '__main__': s = get_suite() print(s) s.countTestCases() runner = unittest.TextTestRunner() runner.run(s) #运行结果: #<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_divide>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_sub>]> #.... #---------------------------------------------------------------------- #Ran 4 tests in 0.000s #OK
法5:使用最简单的方式:unitest.main(),这种方法自动检测测试类中所有以test开头的方法;该方法的优点:
(a)自动查找测试用例
(b)自动构建测试集
(c)自动运行测试用例

import unittest from test_mathfunc import * #(法5)使用最简单的方式:unitest.main(),这种方法自动检测测试类中所有以test开头的方法;该方法的优点: #构建测试集示例5: if __name__ == '__main__': unittest.main() #运行结果: #---------------------------------------------------------------------- #Ran 4 tests in 0.000s #OK
法6:使用unittest.defaultTestLoader.discover构造测试集
用法:discover = unittest.defaultTestLoader.discover(case_dir, pattern="test*.py",top_level_dir=None)
discover方法里面有三个参数:
-case_dir:这个是待执行用例的目录。
-pattern:这个是匹配脚本名称的规则,test*.py意思是匹配test开头的所有脚本。
-top_level_dir:这个是顶层目录的名称,一般默认等于None就行了。

import unittest from test_case_directory import test_mathfunc import os.path import sys #(法6) 使用unittest.defaultTestLoader.discover构造测试集(简化了先要创建测试套件然后再依次加载测试用例) #构建测试集示例6: sys.path.append(os.path.dirname(os.path.realpath(__file__))+ 'test_case_directory') def get_suite(): test_dir = os.path.dirname(os.path.realpath(__file__)) test_dir = os.path.join(test_dir,'test_case_directory') suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py',top_level_dir=None) print('suite:',suite) return suite if __name__ == '__main__': s = get_suite() print(s.countTestCases()) runner = unittest.TextTestRunner() runner.run(s) #运行结果: #Ran 4 tests in 0.000s #suite: <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_divide>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_sub>]>]>]> #4 #OK
2.2 跳过某个测试用例

import unittest from mathfunc import * #定义测试类,父类为unittest.TestCase。 class TestCount(unittest.TestCase): #定义setUp()方法用于测试用例执行前的初始化工作 def setUp(self): #print('setUp') self.obj = Count() #定义多个测试用例,以“test_”开头命名的方法 def test_add(self): self.assertEqual(15, self.obj.add(10, 5)) def test_sub(self): self.assertEqual(5, self.obj.sub(10, 5)) #@unittest.skipIf(Count.version==1,'no test')条件为真时跳过测试用例 #@unittest.skipUnless(Count.version==1,'no test')#条件为真时不跳过测试用例 @unittest.expectedFailure #测试结果与预期值不相同,不计入测试失败统计 def test_multi(self): self.assertEqual(50, self.obj.multi(10, 5)) @unittest.skip('Notest')#无条件跳过该测试用例 def test_divide(self): self.assertEqual(2, self.obj.divide(10, 5)) self.assertEqual(2.5,self.obj.divide(5, 2)) # 定义tearDown()方法用于测试用例执行之后的善后工作。 def tearDown(self): # print('tearDown') self.obj = None
2.3 unittest.mock模块
Mock:向测试对象提供一套和测试资源完全相同的接口和方法,不关心具体实现过程,只关心具体结果
参数 | 说明 |
name | Mock对象的名字 |
spec | Mock对象的属性 |
return_value | Mock对象返回值 |
mock_calls | Mock对象所有调用顺序 |
call_args | Mock对象初始化参数 |
call_args_list | 调用中使用参数 |
call_count | Mock被调用次数 |
assert_called_with(arg) | 检查函数调用参数是否正确 |
assert_called_once_with(arg) | 同上,但是只调用一次 |
assert_has_calls() | 期望调用方法列表 |
示例:用mock模拟云端客户端接口,然后在客户端功能未实现之前,通过模拟的接口进行用例测试

from unittest import mock #模拟云端客户端 class CouldClient(object): def connect(self): pass def disconnect(self): pass def upload(self): pass def download(self): pass tmock=mock.Mock(CouldClient) tmock.connect.return_value = 200 tmock.disconnect.return_value = 404

import unittest from unittest import mock from mockcalss import CouldClient import unittest class TestCould(unittest.TestCase): def setUp(self): self.obj = mock.Mock(CouldClient) def tearDown(self): self.obj = None def test_connect(self): self.obj.connect.return_value = 200 self.assertEqual(self.obj.connect(),200) if __name__ == '__mian__': unittest.main()
2.4 将测试结果输出到文件
(1)将测试结果输出到txt文件
将原来的test_suite.py文件做如下改动,便能将测试结果输出到txt格式的文本中

import unittest from unittest import mock from test_case_directory import test_mathfunc import os.path def get_suite(): test_dir = os.path.dirname(os.path.realpath(__file__)) test_dir = os.path.join(test_dir,'test_case_directory') suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py',top_level_dir=None) print('suite:',suite) return suite if __name__ == '__main__': s = get_suite() print(s.countTestCases()) with open('unittestTestReport.txt','a')as f: #注意:verbosity参数可以控制输出的错误报告的详细程度,只有3个取值 #0 (quiet): 只显示执行的用例的总数和全局的执行结果。 #1 (default): 默认值,显示执行的用例的总数和全局的执行结果,并对每个用例的执行结果(成功T或失败F)有个标注。 #2 (verbose): 显示执行的用例的总数和全局的执行结果,并输出每个用例的详细的执行结果。 runner = unittest.TextTestRunner(stream=f,verbosity=2) runner.run(s)
执行此文件,可以看到,在同目录下生成了如下所示的UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了

test_add (test_mathfunc.TestCount) ... ok test_divide (test_mathfunc.TestCount) ... ok test_multi (test_mathfunc.TestCount) ... ok test_sub (test_mathfunc.TestCount) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
(2)借助HTMLTestRunner生成漂亮的HTML报告
txt格式的文本执行报告过于简陋,这里我们学习一下借助HTMLTestRunner生成HTML报告。首先需要下载HTMLTestRunner.py,并放到当前目录下,或者python目录下的Lib中,就可以导入运行了。
将原来的test_suite.py文件做如下改动,便能将测试结果输出到HTML格式的文本中

#_*_ coding=utf-8 _*_ import unittest from unittest import mock from test_case_directory import test_mathfunc import HTMLTestRunner import os.path def get_suite(): test_dir = os.path.dirname(os.path.realpath(__file__)) test_dir = os.path.join(test_dir,'test_case_directory') suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py',top_level_dir=None) print('suite:',suite) return suite if __name__ == '__main__': s = get_suite() print(s.countTestCases()) with open('HTMLReport.html','wb')as f: #注意:verbosity参数可以控制输出的错误报告的详细程度,只有3个取值 #0 (quiet): 只显示执行的用例的总数和全局的执行结果。 #1 (default): 默认值,显示执行的用例的总数和全局的执行结果,并对每个用例的执行结果(成功T或失败F)有个标注。 #2 (verbose): 显示执行的用例的总数和全局的执行结果,并输出每个用例的详细的执行结果。 runner = HTMLTestRunner.HTMLTestRunner(stream=f,title='MathFunc Test Report',description='generated by HTMLTestRunner.',verbosity=2) runner.run(s)
输出 的测试报告如下图:
(3)增加测试报告的可读性
虽然在我们在测试用例开发时为每个用例添加了注释,但测试报告一般是给非测试人员阅读的,如果能在报告中为每一个测试用例添加说明,那么将会使报告更加易于阅读,
打开我们的测试用例文件,为每一个测试用例(方法)下面添加注释,如下:

import unittest from conut import * #定义测试类,父类为unittest.TestCase。 class TestCount(unittest.TestCase): #定义setUp()方法用于测试用例执行前的初始化工作 """测试类:Conut""" def setUp(self): #print('setUp') self.obj = Count() #定义多个测试用例,以“test_”开头命名的方法 def test_add(self): """测试加法""" self.assertEqual(10, self.obj.add(10, 5)) def test_sub(self): """测试减法""" self.assertEqual(5, self.obj.sub(10, 5)) def test_multi(self): """测试乘法""" self.assertEqual(50, self.obj.multi(10, 5)) def test_divide(self): """测试除法""" self.assertEqual(2, self.obj.divide(10, 5)) self.assertEqual(2.5,self.obj.divide(5, 2)) # 定义tearDown()方法用于测试用例执行之后的善后工作。 def tearDown(self): #print('tearDown') self.obj = None
现在生成的测试报告中将会有注释信息,如下:
三 . 将测试结果通过邮件发送
使用python3的email模块和smtplib模块可以实现发送邮件的动能。email模块用来生成email,smtplib模块用来发送邮件,接下来看如何在生成测试报告之后,将报告放在邮件附件中并发送给项目组的人
SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。
python的smtplib提供了一种很方便的途径发送电子邮件。它对smtp协议进行了简单的封装。
Python创建 SMTP 对象语法如下:
import smtplib smtpObj = smtplib.SMTP( [host [, port [, local_hostname]]] )
参数说明:
- host: SMTP 服务器主机。 你可以指定主机的ip地址或者域名如: runoob.com,这个是可选参数。
- port: 如果你提供了 host 参数, 你需要指定 SMTP 服务使用的端口号,一般情况下 SMTP 端口号为25。
- local_hostname: 如果 SMTP 在你的本机上,你只需要指定服务器地址为 localhost 即可。
Python SMTP 对象使用 sendmail 方法发送邮件,语法如下:
SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options])
参数说明:
- from_addr: 邮件发送者地址。
- to_addrs: 字符串列表,邮件发送地址。
- msg: 发送消息
这里要注意一下第三个参数,msg 是字符串,表示邮件。我们知道邮件一般由标题,发信人,收件人,邮件内容,附件等构成,发送邮件的时候,要注意 msg 的格式。这个格式就是 smtp 协议中定义的格式。
以下执行实例需要你本机已安装了支持 SMTP 的服务,如:sendmail。
以下是一个使用 Python 发送邮件简单的实例:

#!/usr/bin/python # -*- coding: UTF-8 -*- import smtplib from email.mime.text import MIMEText from email.header import Header sender = 'from@runoob.com' receivers = ['429240967@qq.com'] # 接收邮件,可设置为你的QQ邮箱或者其他邮箱 # 三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码 message = MIMEText('Python 邮件发送测试...', 'plain', 'utf-8') message['From'] = Header("菜鸟教程", 'utf-8') # 发送者 message['To'] = Header("测试", 'utf-8') # 接收者 subject = 'Python SMTP 邮件测试' message['Subject'] = Header(subject, 'utf-8') try: smtpObj = smtplib.SMTP('localhost') smtpObj.sendmail(sender, receivers, message.as_string()) print "邮件发送成功" except smtplib.SMTPException: print "Error: 无法发送邮件"
如果我们本机没有 sendmail 访问,也可以使用其他邮件服务商的 SMTP 访问(QQ、网易、Google等)。

import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.utils import formataddr import os.path from constant.constant import REPORT_PATH from utils.common.log import logger # 使用第三方邮件服务商的SMTP发送邮件 #QQ 邮箱 SMTP 服务器地址:smtp.qq.com,ssl 端口:465。 mail_host = "smtp.qq.com" # 设置服务器 sender = "xxxxxxxx@qq.com" # 用户名 password = "xxxxxxxxxxxx" # QQ邮箱的SMTP授权码 receivers = ['xxxxxxxxx@qq.com','xxxxxxxxxxx@163.com'] # 接收邮件,可设置为你的QQ邮箱或者其他邮箱 file = os.path.join(REPORT_PATH, 'ExampleReport.html')#测试报告地址 def mail(): # 创建一个带附件的实例 message = MIMEMultipart() # 括号里的对应发件人邮箱昵称、发件人邮箱账号 message['From'] = formataddr(['发件人姓名',sender]) logger.info('发件人邮箱:%s' % sender) # 括号里的对应收件人邮箱昵称、收件人邮箱账号 #单个收件人: message['To'] = formataddr(['收件人姓名',sender]) #多个收件人: message['To'] = ';'.join(receivers) logger.info('收件人邮箱:%s' % receivers) # 邮件的主题,也可以说是标题 message['Subject'] = 'Python SMTP 邮件测试' # 邮件正文内容 message.attach(MIMEText('Python 邮件发送测试...', 'plain', 'utf-8')) # 构造附件1 att1 = MIMEText(open(file, 'rb').read(), 'base64', 'utf-8') logger.info('读取附件') att1["Content-Type"] = 'text/html' # filename是附件名,附件名称为中文时的写法 #att1.add_header("Content-Disposition", "attachment", filename=("gbk", "", "xxx接口自动化测试报告.html")) # 附件名称非中文时的写法 att1["Content-Disposition"] = 'attachment; filename="ExampleReport.html")' #添加附件 message.attach(att1) logger.info('添加附件') try: # 发件人邮箱中的SMTP服务器,一般端口是25 server = smtplib.SMTP_SSL(mail_host, 465) logger.info('连接QQ邮箱smtp服务') # 括号中对应的是发件人邮箱账号、邮箱密码 server.login(sender, password) logger.info('连接成功') # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件 server.sendmail(sender, receivers, message.as_string()) # 关闭连接 server.quit() logger.info("邮件发送成功") except Exception: logger.error("邮件发送失败", exc_info=1)
成功发送邮件后,接受邮箱的信息显示如下:
将上面发送邮件的过程抽象成一个类:

class Email: def __init__(self, server, sender, password, receiver, title, message=None, path=None): """初始化Email :param title: 邮件标题,必填。 :param message: 邮件正文,非必填。 :param path: 附件路径,可传入list(多附件)或str(单个附件),非必填。 :param server: smtp服务器,必填。 :param sender: 发件人,必填。 :param password: 发件人SMTP授权码,必填。 :param receiver: 收件人,多收件人用“;”隔开,必填。 """ self.title = title self.message = message self.files = path self.msg = MIMEMultipart('related') self.server = server self.sender = sender self.receiver = receiver self.password = password def _attach_file(self, att_file): """将单个文件添加到附件列表中""" att = MIMEText(open('%s' % att_file, 'rb').read(), 'plain', 'utf-8') att["Content-Type"] = 'application/octet-stream' file_name = re.split(r'[\\|/]', att_file) att["Content-Disposition"] = 'attachment; filename="%s"' % file_name[-1] self.msg.attach(att) logger.info('attach file {}'.format(att_file)) def send(self): self.msg['Subject'] = self.title self.msg['From'] = self.sender self.msg['To'] = self.receiver # 邮件正文 if self.message: self.msg.attach(MIMEText(self.message)) # 添加附件,支持多个附件(传入list),或者单个附件(传入str) if self.files: if isinstance(self.files, list): for f in self.files: self._attach_file(f) elif isinstance(self.files, str): self._attach_file(self.files) # 连接服务器并发送 try: smtp_server = smtplib.SMTP(self.server) # 连接sever except (gaierror and error) as e: logger.exception('发送邮件失败,无法连接到SMTP服务器,检查网络以及SMTP服务器. %s', e) else: try: smtp_server.login(self.sender, self.password) # 登录 except smtplib.SMTPAuthenticationError as e: logger.exception('用户名密码验证失败!%s', e) else: smtp_server.sendmail(self.sender, self.receiver.split(';'), self.msg.as_string()) # 发送邮件 finally: smtp_server.quit() # 断开连接 logger.info('发送邮件"{0}"成功! 收件人:{1}。如果没有收到邮件,请检查垃圾箱,' '同时检查收件人地址是否正确'.format(self.title, self.receiver)) if __name__ == '__main__': # 使用第三方邮件服务商的SMTP发送邮件 #QQ 邮箱 SMTP 服务器地址:smtp.qq.com,ssl 端口:465。 mail_host = "smtp.qq.com" # 设置服务器 sender = "xxxxxxxxx@qq.com" # 用户名 password = "xxxxxxxxxxxxxxxxx" # QQ邮箱的SMTP授权码 receivers = ['xxxxxxxxx@qq.com','xxxxxxxxxxx@163.com'] # 接收邮件,可设置为你的QQ邮箱或者其他邮箱 file = os.path.join(REPORT_PATH, 'ExampleReport.html')#测试报告地址 e = Email(title='测试报告', message='这是今天的测试报告,请查收!', receiver=';'.join(receivers), server=mail_host, sender=sender, password=password, path=file ) e.send()
>>>>>>>待续
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构