unittest单元测试框架
概述
unittest是Python内置的单元测试框架,不仅可以完成单元测试,也适用于自动化测试中。
unittest提供了丰富的断言方法,判断测试用例是否通过,然后生成测试结果报告。
官网帮助文档:https://docs.python.org/zh-cn/3/library/unittest.html
单元测试框架主要用来完成以下三件事:
- 提供用例组织与执行:当测试用例只有几条时,可以不必考虑用例的组织,但是当用例达到成百上千条时,大量的用例堆砌在一起,就产生了扩展性与维护性等,单元测试框架就是用来解决用例的规范与组织问题。
- 提供了丰富的比较方法:不论是功能测试,还是单元测试,在用例执行完成之后都需要将实际结果与预期结果进行比较(断言),从而判定用例是否执行通过。单元测试框架一般会提供丰富的断言方法。例如:判断相等/不等、包含/不包含、True/False的断言方法等。
- 提供丰富的日志:当测试用例执行失败时能抛出清晰的失败原因,当所有用例执行完成后提供丰富的执行结果。例如:,总执行时间、失败用例数、成功用例数等。
unnittest介绍
什么是单元测试?
单元测试负责对最小的软件设计单元(模块)进行验证,它使用软件设计文档中对模块的描述作为指南,对重要的程序分支进行测试以发现模块中的错误。
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/2 15:27' class Count: def __init__(self, a, b): self.a = int(a) self.b = int(b) def calculate(self): return self.a + self.b
test.py
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 11:56' import unittest # 导入unittest框架 from compute import Count # 导入用例集 class TestCount(unittest.TestCase): def setUp(self):
"""用例初始化""" print("test start") def test_add(self): """执行用例1""" print(Count(2, 3)) self.assertEqual(Count(2, 3).calculate(), 5) def tearDown(self):
"""用例执行完毕,收尾""" print("test end ") if __name__ == '__main__': unittest.main() # 固定调用方法
用例执行流程:
- setUp初始化方法第一个执行,处理一些初始化操作。
- 接着runTest执行用例,用例返回True。
- 最后,tearDown打扫战场!
在每个用例执行时,setUp和tearDown都会执行。
注意:
- myUnitTest类名可以自定义,但是必须继承unittest.TestCase
- 示例中的setUp和tearDown方法名是固定的,但测试用例,没有初始化和收尾的工作,setUp和tearDown方法可以省略不写。
- 测试用例必须已test开头,unittest.main()就可以认识然后直接运行。
unittest的断言
unittest.TestCase提供了一些断言方法用来检查并报告故障
最常用的方法:
示例
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 12:09' import unittest class TestStringMethods(unittest.TestCase): def test_assertEqual(self): self.assertEqual(1, 2, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2 def test_assertTrue(self): self.assertTrue('') def test_assertFalse(self): self.assertFalse('') if __name__ == '__main__': unittest.main()
所有的assert方法都接收一个msg参数,如果指定,该参数将用作失败时的错误提示。
结果:
F.F ====================================================================== FAIL: test_assertEqual (__main__.TestStringMethods) ---------------------------------------------------------------------- Traceback (most recent call last): File "s7.py", line 11, in test_assertEqual self.assertEqual(1, 2, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2 AssertionError: 1 != 2 : 1 != 2 ====================================================================== FAIL: test_assertTrue (__main__.TestStringMethods) ---------------------------------------------------------------------- Traceback (most recent call last): File "s7.py", line 14, in test_assertTrue self.assertTrue('') AssertionError: '' is not true ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=2)
F.F表示,如果用例通过返回.,失败返回F,结果告诉3个用例成功1个,失败2个。
如何在用例中输出用例名和用例描述信息
self._testMethodName 用例名
self._testMethodDoc 用例的描述信息,即方法的注释内容
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 12:09' import unittest class TestStringMethods(unittest.TestCase): def test_assertEqual(self): """描述信息""" print(self._testMethodName, self._testMethodDoc) self.assertEqual(1, 1, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2 if __name__ == '__main__': unittest.main()
unittestTestSute
TestSuite是测试套件,承载多个用例的集合,该集合中有多个用例,可以理解为一个盒子,该盒子中存放多个用例
当所有用例都添加到了盒子中,然后找一个执行器,去执行盒子中的测试用例。
流程:
实例化所有的用例
- 创建一个盒子
- 将用例添加到盒子中
- 将所有用例都收集到盒子中后,使用执行器执行盒子中的测试用例
使用addTests
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 12:09' import unittest class MyCase(unittest.TestCase): def test_is_upper(self): self.assertTrue("Foo".isupper()) def test_is_lower(self): self.assertTrue("foo".islower()) if __name__ == '__main__': # 实例化用例 case_01 = MyCase(methodName="test_is_upper") case_02 = MyCase(methodName="test_is_lower") # 创建suite容器 suite = unittest.TestSuite() # 将用例添加到盒子中 # 方式1:每个用例单独添加 # suite.addTest(case_01) # suite.addTest(case_02) # 方式2:用例对象已列表类型添加 suite.addTests([case_01, case_02]) # 使用执行器执行盒子suite中的用例 runner = unittest.TextTestRunner() runner.run(suite)
使用Map
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 12:09' import unittest class MyCase(unittest.TestCase): def test_is_upper(self): self.assertTrue("Foo".isupper()) def test_is_lower(self): self.assertTrue("foo".islower()) if __name__ == '__main__': # 实例化用例 case_obj = map(MyCase, ["test_is_upper", "test_is_lower"]) print(case_obj, list(case_obj)) # 创建suite容器 suite = unittest.TestSuite() # 将用例添加到盒子中 suite.addTests(case_obj) # 返回suite中测试用例个数 print(suite.countTestCases()) # 使用执行器执行盒子suite中的用例 runner = unittest.TextTestRunner() runner.run(suite)
unittest.TestSuite中方法:
- addTest 一个一个添加
- addTests 批量添加
- suite.countTestCases() 用例个数
unittest.makeSuite
unittest.makeSuite实例化suite时,同时进行测试用例收集,返回收集完成的suite,最后交给执行器去执行。
unittest其它方法
发现其它目录中脚本用例:
unittest.TestLoader().loadTestsFromModule(),找到到指定模块下面的TestCase子类,获取其中以test开头的用例。
suite = unittest.TestLoader().loadTestsFromModule(test_case) # 执行test_case.py文件中的用例,如果在其它目录需要from导入 # 执行器执行 unittest.TextTestRunner(verbosity=2).run(suite)
(单个用例)执行指定模块名、类名、用例名进行执行
from scripts import ff_case suit = unittest.TestLoader().loadTestsFromName( name="MyTestCase.test_case_01", # 类.用例名称 module=ff_case # 模块名 ) unittest.TextTestRunner( verbosity=2 ).run(suit)
(多个用例)执行指定模块名、类名、多条用例名进行执行
from scripts import ff_case suit = unittest.TestLoader().loadTestsFromNames( names=[ # 列表形式指定多条用例 "MyTestCase.test_case_01", # ff_case.py -->MyTestCae类-->test_case_01用例 "MyTestCae.test_case_02" # ff_case.py -->MyTestCae类-->test_case_02用例 ], module=ff_case # 指定模块名 ) unittest.TextTestRunner( verbosity=2 ).run(suit)
unittest.TestLoader().discover(),找到指定目录中,指定测试用例
注意:discover只会收集Python包中以pattern开头的脚本,再找脚本中unittest.TestCase子类中以test开头的测试用例
# 发现指定目录中所有合法脚本中合法的测试用例 unittest.TestLoader().discover( top_level_dir="", # 顶级目录 start_dir="", #指定目录 pattern="*_test.py",# 匹配文件模式 )
setUpClass 和 tearDownClass
测试某一个用例时,都会对应的执行三个方法:
- setUp,开头一枪的那家伙,它负责该用例之前可能需要的一些准备,比如连接数据库。
- runTest,执行用例逻辑,没的说,干活的长工。
- tearDown,负责打扫战场,比如关闭数据库连接。
compute.py
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 12:09' import unittest import compute class myUnitTest(unittest.TestCase): def test_add(self): self.assertEqual(compute.add(2, 3), 5) def test_sub(self): self.assertEqual(compute.sub(10, 5), 5) def setUp(self): """测试用例之前执行,无论我在myUnitTest的什么位置""" print("打开数据库") def tearDown(self): """测试用例之后执行,无论我在myUnitTest的什么位置""" print("关闭数据库") if __name__ == '__main__': unittest.main()
结果:
打开数据库 关闭数据库 .打开数据库 关闭数据库 . ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
两个用例被执行并通过,并且每一个用例执行前后都触发了setUp和tearDown方法执行。如果这是由1000个甚至更多的用例组成的用例集,并且每一个用例都去操作数据,那么每个用例都会做连接/关闭数据库的操作。这就有问题了,就不能一次连接,所有用例完事后,再关闭。
解决方案:
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = 'tian' __data__ = '2021/3/3 12:09' import unittest import compute class myUnitTest(unittest.TestCase): def test_add(self): self.assertEqual(compute.add(2, 3), 5) def test_sub(self): self.assertEqual(compute.sub(10, 5), 5) @classmethod def setUpClass(cls): """测试用例之前执行,无论我在myUnitTest的什么位置""" print("在用例集开始执行,我去建立数据库连接...") @classmethod def tearDownClass(cls): """测试用例之后执行,无论我在myUnitTest的什么位置""" print("全军撤退,关闭数据库") if __name__ == '__main__': unittest.main(verbosity=2)
结果:
在用例集开始执行,我去建立数据库连接... test_add (__main__.myUnitTest) ... ok test_sub (__main__.myUnitTest) ... ok 全军撤退,关闭数据库 ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
由结果可以看到,setUpClass和tearDownClass这两个类方法完美的解决了问题,让我们在某些情况下可用更加灵活的组织逻辑。
verbosity参数
verbosity控制错误输出的详细程度:
在执行unittest.main(verbosity=1)时,可以通过verbosity参数来控制错误信息的详细程度。
verbosity=0
在用例集开始执行,我去建立数据库连接... 全军撤退,关闭数据库 ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
verbosity=1
在用例集开始执行,我去建立数据库连接... ..全军撤退,关闭数据库 ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
verbosity=2
在用例集开始执行,我去建立数据库连接... test_add (__main__.myUnitTest) ... ok test_sub (__main__.myUnitTest) ... ok 全军撤退,关闭数据库 ---------------------------------------------------------------------- Ran 2 tests in 0.002s OK
由结果总结,verbosity有3种错误信息状态提示:
- 0,静默模式,对于测试结果给予简单提示。
- 1,默认模式,与静默模式类似,只是在每个成功的用例前面有个
.
每个失败的用例前面有个F
,跳过的用例有个S
。 - 2,详细模式,测试结果会显示每个用例的所有相关的信息。
切记,只有0、1、2
三种状态。
默认的是1。
-v
除此之外,我们在终端执行时也可以输出详细报告:
E:\testselenium>python s7.py -v 在用例集开始执行,我去建立数据库连接... test_add (__main__.myUnitTest) ... ok test_sub (__main__.myUnitTest) ... ok 全军撤退,关闭数据库 ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
如上示例,使verbosity
参数保持默认,我们通过在终端加-v来输入详细报告信息。
skip跳过测试用例
unittest支持跳过单个测试方法甚至整个测试类。
我们可以使用unittest提供的相关装饰器来完成:
示例:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'tian'
__data__ = '2021/3/3 14:32'
import unittest
class TestCase01(unittest.TestCase):
def test_assertTrue(self):
self.assertTrue(" ")
@unittest.skip("跳过该条用例")
def test_assertFalse(self):
self.assertFalse(" ")
@unittest.skip("跳过这个用例类")
class TestCase02(unittest.TestCase):
def test_assertTrue(self):
self.assertTrue("")
def test_assertFalse(self):
self.assertFalse("")
if __name__ == '__main__':
unittest.main()
结果:
E:\testselenium>python s8.py -v test_assertFalse (__main__.TestCase01) ... skipped '跳过该条用例' test_assertTrue (__main__.TestCase01) ... ok test_assertFalse (__main__.TestCase02) ... skipped '跳过这个用例类' test_assertTrue (__main__.TestCase02) ... skipped '跳过这个用例类' ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK (skipped=3)
共4个用例,一个用例类被跳过,另一个用例类中跳过一个方法,那么就是执行4个用例,跳过3个。
用例结果输出文件中
#!/usr/bin/env python # -*- coding: utf-8 -*- # author: 青城子 # datetime: 2022/2/10 14:44 # ide: PyCharm import unittest class MyCase(unittest.TestCase): def test_case_01(self): self.assertTrue(1) # @unittest.skip(reason="无条件跳过") def test_case_02(self): self.assertTrue("") # @unittest.skipIf(condition=3 < 2, reason="有条件跳过") def test_case_03(self): self.assertTrue(0) if __name__ == '__main__': suite = unittest.makeSuite(testCaseClass=MyCase, prefix="test") print(suite) # unittest.TextTestRunner().run(suite) # 执行屏幕输出 # 将屏幕输出转换成文本输出 with open("a.txt", "w") as f: unittest.TextTestRunner(stream=f).run(suite)
主要使用stream参数,给一个文件句柄。