Unittest
1. unittest 简介
2. unittest 代码示例
3. 测试报告(unittest + HTMLTestRunner)
1. unittest 简介
1.1 什么是 unittest?
unittest 是 python 自带的一个单元测试框架,类似于 java 的 junit,基本结构是类似的。
unittest 中有 5 个重要的概念:TestCase、TestSuite、TestRunner、TestLoader、TestFixture
- Testcase:一个 TestCase 的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。单元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
- TestSuite:多个测试用例集合在一起,就是 TestSuite,而且 TestSuite 也可以嵌套 TestSuite。
- TestRunner:是来执行测试用例的,其中的 run(test)会执行 TestSuite/TestCase 中的 run(result)方法。
- TestLoader:是用来加载 TestCase 到 TestSuite 中的,其中有几个 loadTestsFrom__() 方法,就是从各个地方寻找 TestCase,创建它们的实例,然后 add 到 TestSuite 中,再返回一个 TestSuite 实例。
- TestFixture:对一个测试用例环境的搭建和销毁,是一个 fixture,通过覆盖 TestCase 的 setUp() 和 tearDown() 方法来实现。这个有什么用呢?比如说在这个测试用例中需要访问数据库,那么可以在 setUp() 中建立数据库连接以及进行一些初始化,在 tearDown() 中清除在数据库中产生的数据,然后关闭连接。注意 tearDown 的过程很重要,要为以后的 TestCase 留下一个干净的环境。关于 fixture,还有一个专门的库函数叫做 fixtures,功能更加强大。
1.2 用法
- 用 import unittest 导入 unittest 模块;
- 定义一个继承自 unittest.TestCase 的测试用例类,如 class xxx(unittest.TestCase);
- 定义 setUp 和 tearDown,这两个方法与 junit 相同,即如果定义了则会在每个测试 case 执行前先执行 setUp 方法,执行完毕后执行 tearDown 方法;
- 定义测试用例,名字以 test 开头,unittest 会自动将 test 开头的方法放入测试用例集中;
- 一个测试用例应该只测试一个任务/方面/场景,测试目的和测试内容应很明确。并主要调用 assertEqual、assertRaises 等断言方法来判断程序执行结果和预期值是否相符;
- 调用 unittest.main() 启动测试;
- 如果测试未通过,则会显示 E,并给出具体的错误(此处为程序问题导致)。如果测试失败则显示为 F,测试通过为 . 。如有多个 testcase,则结果依次显示。
1.3 常用断言方法
- 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
- assertIsNot(a, b) 即 a is not b
- assertIsNone(x) 即 x is None
- assertIsNotNone(x) 即 x is not None
- assertIn(a, b) 即 a in b
- assertNotIn(a, b) 即 a not in b
- assertIsInstance(a, b) 即 isinstance(a, b)
- assertNotIsInstance(a, b) 即 not isinstance(a, b)
2. unittest 代码示例
2.1 TestCase(测试用例)
- setUpClass() 和 tearDownClass() 方法在整个测试类运行过程中只被执行一次。
- setUp() 和 tearDown() 在每个测试用例方法执行前和后均被调用。
1 import unittest 2 3 4 class mytest(unittest.TestCase): 5 6 @classmethod 7 def setUpClass(cls): 8 '''初始化类固件''' 9 pass 10 11 @classmethod 12 def tearDownClass(cls): 13 '''重构类固件''' 14 pass 15 16 # 初始化工作 17 def setUp(self): 18 pass 19 20 # 退出清理工作 21 def tearDown(self): 22 pass 23 24 # 具体的测试用例,一定要以test开头 25 def testSum(self): 26 assert 1+1 == 2 27 28 def testSub(self): 29 assert 1-1 == 1 30 31 32 if __name__ == '__main__': 33 # 启动单元测试 34 unittest.main()
执行结果
在测试结果中,一个 F 表示一个测试方法执行不通过,一个 . 表示一个测试方法执行通过,共执行了两个测试方法。
F. ====================================================================== FAIL: testSub (__main__.mytest) ---------------------------------------------------------------------- Traceback (most recent call last): File "code.txt", line 29, in testSub assert 1-1 == 1 AssertionError ---------------------------------------------------------------------- Ran 2 tests in 0.002s FAILED (failures=1)
2.2 TestSuite(测试集合)
1 import unittest 2 3 4 class MyTestCase1(unittest.TestCase): 5 6 def setUp(self): 7 pass 8 9 def tearDown(self): 10 pass 11 12 def testSum(self): 13 pass 14 15 def testSub(self): 16 pass 17 18 19 class MyTestCase2(unittest.TestCase): 20 21 def setUp(self): 22 pass 23 24 def tearDown(self): 25 pass 26 27 def testSum2(self): 28 pass 29 30 def testSub2(self): 31 pass 32 33 34 if __name__ == '__main__': 35 # 根据给定的测试类,获取其中所有的'test'开头的测试方法,并返回一个测试套件 36 testCase1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase1) 37 testCase2 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase2) 38 # 将多个测试套件组装成一个 39 suite = unittest.TestSuite([testCase1, testCase2]) 40 # 设置verbosity=2可以打印更详细的执行信息 41 unittest.TextTestRunner(verbosity=2).run(suite)
执行结果
testSub (__main__.MyTestCase1) ... ok testSum (__main__.MyTestCase1) ... ok testSub2 (__main__.MyTestCase2) ... ok testSum2 (__main__.MyTestCase2) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.006s OK
2.3 按指定顺序执行测试方法
以 unittest.main() 方法启动的单元测试,各测试方法的执行顺序是以所有测试方法名的字符串的 ASCII 码排序后的顺序。
若想按顺序执行,则可按执行顺序增加测试方法到测试集合中。
1 import unittest 2 3 4 class MyTestCase1(unittest.TestCase): 5 6 def setUp(self): 7 pass 8 9 def tearDown(self): 10 pass 11 12 def testSum(self): 13 pass 14 15 def testSub(self): 16 pass 17 18 19 class MyTestCase2(unittest.TestCase): 20 21 def setUp(self): 22 pass 23 24 def tearDown(self): 25 pass 26 27 def testSum2(self): 28 pass 29 30 def testSub2(self): 31 pass 32 33 34 if __name__ == '__main__': 35 suite = unittest.TestSuite() 36 37 # 将测试用例按执行顺序加载到测试集合中 38 suite.addTest((MyTestCase2('testSum2'))) 39 suite.addTest((MyTestCase2('testSub2'))) 40 suite.addTest((MyTestCase1('testSum'))) 41 suite.addTest((MyTestCase1('testSub'))) 42 43 unittest.TextTestRunner(verbosity=2).run(suite)
执行结果
testSum2 (__main__.MyTestCase2) ... ok testSub2 (__main__.MyTestCase2) ... ok testSum (__main__.MyTestCase1) ... ok testSub (__main__.MyTestCase1) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.007s OK
2.4 设置条件忽略指定测试方法
1 import unittest 2 import sys 3 4 5 class MyTestCase1(unittest.TestCase): 6 7 a = 6 8 9 def setUp(self): 10 pass 11 12 def tearDown(self): 13 pass 14 15 # 无条件忽略该测试方法 16 @unittest.skip("skipping") 17 def testSum(self): 18 pass 19 20 # 如果变量a>5,则忽略该测试方法 21 @unittest.skipIf(a>5, "condition is not satisfied!") 22 def testSub(self): 23 pass 24 25 26 class MyTestCase2(unittest.TestCase): 27 28 def setUp(self): 29 pass 30 31 def tearDown(self): 32 pass 33 34 # 除非平台是Linux,否则忽略该测试方法 35 @unittest.skipUnless(sys.platform.startswith("linux"), "requires linux") 36 def testSum2(self): 37 pass 38 39 def testSub2(self): 40 pass 41 42 43 if __name__ == '__main__': 44 suite = unittest.TestSuite() 45 46 # 将测试用例按执行顺序加载到测试集合中 47 suite.addTest((MyTestCase2('testSum2'))) 48 suite.addTest((MyTestCase2('testSub2'))) 49 suite.addTest((MyTestCase1('testSum'))) 50 suite.addTest((MyTestCase1('testSub'))) 51 52 unittest.TextTestRunner(verbosity=2).run(suite)
执行结果
testSum2 (__main__.MyTestCase2) ... skipped 'requires linux' testSub2 (__main__.MyTestCase2) ... ok testSum (__main__.MyTestCase1) ... skipped 'skipping' testSub (__main__.MyTestCase1) ... skipped 'condition is not satisfied!' ---------------------------------------------------------------------- Ran 4 tests in 0.008s OK (skipped=3)
2.5 批量加载测试模块
执行文件
1 import unittest 2 3 4 if __name__ == '__main__': 5 6 # 加载当前目录下所有有效的测试模块,"."表示当前目录 7 testSuite = unittest.TestLoader().discover('.') 8 unittest.TextTestRunner(verbosity=2).run(testSuite)
同一目录下的 TestCase1.py
1 import unittest 2 3 class MyTestCase1(unittest.TestCase): 4 5 def setUp(self): 6 pass 7 8 def tearDown(self): 9 pass 10 11 def testSum(self): 12 pass 13 14 def testSub(self): 15 pass
同一目录下的 TestCase2.py
1 import unittest 2 3 class MyTestCase2(unittest.TestCase): 4 5 def setUp(self): 6 pass 7 8 def tearDown(self): 9 pass 10 11 def testSum2(self): 12 pass 13 14 def testSub2(self): 15 pass
执行结果
testSub (TestCase1.MyTestCase1) ... ok testSum (TestCase1.MyTestCase1) ... ok testSub2 (TestCase2.MyTestCase2) ... ok testSum2 (TestCase2.MyTestCase2) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.007s OK
3. 测试报告(unittest + HTMLTestRunner)
HTMLTestRunner 模块的下载地址:https://www.cnblogs.com/benpao1314/p/9633862.html
1 import unittest 2 import HTMLTestRunner # 引入报告模板 3 import math 4 import ddt # 引入数据驱动 5 6 7 class Calc(object): 8 9 def add(self, x, y, *d): 10 # 加法计算 11 result = x + y 12 for i in d: 13 result += i 14 return result 15 16 def sub(self, x, y, *d): 17 # 减法计算 18 result = x - y 19 for i in d: 20 result -= i 21 return result 22 23 24 class SuiteTestCalc(unittest.TestCase): 25 def setUp(self): 26 self.c = Calc() 27 28 @unittest.skip("skipping") 29 def test_Sub(self): 30 print ("sub") 31 self.assertEqual(self.c.sub(100, 34, 6), 61, u'求差结果错误!') 32 33 def testAdd(self): 34 print ("add") 35 self.assertEqual(self.c.add(1, 32, 56), 89, u'求和结果错误!') 36 37 38 @ddt.ddt 39 class SuiteTestPow(unittest.TestCase): 40 41 def setUp(self): 42 self.seq = list(range(10)) 43 44 # @unittest.skipIf() 45 def test_Pow(self): 46 print ("Pow") 47 self.assertEqual(pow(6, 3), 2161, u'求幂结果错误!') 48 49 # 使用数据驱动传参可以让函数执行,实际上函数可并不需要用到传入的参数 50 # 一组数据会执行一次函数,因此ddt实际上可实现循环的效果 51 # @ddt.data(*[[i] for i in range(500)]) 实现500次循环执行该函数 52 @ddt.data(['t1' ,'r1'] , 53 ['t2' , 'r2']) 54 @ddt.unpack 55 def test_hasattr(self,data1,data2): 56 print ("hasattr") 57 # 检测math模块是否存在pow属性 58 self.assertTrue(hasattr(math, 'pow'), u"检测的属性不存在!") 59 60 61 if __name__ == "__main__": 62 suite1 = unittest.TestLoader().loadTestsFromTestCase(SuiteTestCalc) 63 suite2 = unittest.TestLoader().loadTestsFromTestCase(SuiteTestPow) 64 suite = unittest.TestSuite([suite1, suite2]) 65 #unittest.TextTestRunner(verbosity=2).run(suite) 66 filename = "e:\\test.html" # 定义报告的存放路径(支持相对路径) 67 # 以二进制方式打开文件 68 fp = open(filename, 'wb') 69 # 使用HTMLTestRunner配置参数,输出报告路径、报告标题、描述等。均可自定义配置 70 runner = HTMLTestRunner.HTMLTestRunner(stream = fp, title = '测试报告', description = '测试报告内容') 71 # 运行测试集合 72 runner.run(suite) 73 fp.close()
执行结果
<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'> Time Elapsed: 0:00:00.002025 .F..
生成的 test.html 文件