软件测试——Unittest单元测试框架详解
一,前言
1,单元测试
软件测试一般按阶段划分为:单元测试,集成测试,系统测试。单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证。 单元测试中单元的含义,单元就是人为规定的最小的被测功能模块,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。在实际项目中,单元测试往往由开发人员完成。
2,单元测试框架
-
单元测试其实就是构造数据使用一段代码去测试另一段代码,理论上来说,不使用单元测试框架也能进行单元测试。但如果用于单元测试的代码(即测试用例)增多,在没有测试框架的情况下会变得拥挤、不可管理,这个时候引入测试框架就变得尤为重要。
-
单元测试框架提供了一种统一的编程模型,可以将测试定义为一些简单的类,这些类中的方法可以调用希望测试的应用程序代码。利用单元测试框架,可以很轻松地插入、设置和分解有关测试的功能,可以直观方便地管理测试用例。
-
主流的单元测试框架,如Java的Junit、TestNg,python的Unittest、Pyunit、Pytest,通用的自动化测试框架Robot Framework等。
3,单元测试框架作用
-
提供用例组织与执行
-
提供丰富的断言方法
-
提供丰富的日志与测试结果
二,Unittest 测试框架
1,Unittest 简介
Unittest是Python自带的单元测试框架,不仅适用于单元测试,还可用于Web、Appium、接口自动化测试用例的开发与执行。该测试框架可组织执行测试用例,并且提供丰富的断言方法,判断测试用例是否通过,并最终生成测试结果。
Unittest官方文档:unittest — Unit testing framework — Python 3.10.4 documentation
2,Unittest 核心要素
-
TestCase:即测试用例,Unittest提供testCase类来编写测试用例,一个TestCase的实例就是一个测试用例。一条测试用例就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown),通过运行一条测试用例,可以对某一个问题进行验证。
-
Fixture:即测试固件,用于测试用例环境的搭建和销毁。在测试步骤执行前需要为该测试用例准备环境(SetUp),如启动app或打开浏览器,测试步骤执行后需要恢复环境 (TearDown),如关闭app或浏览器,这时候就需要用到Fixture,使代码更简洁。
-
TestSuite:即测试套件,把需要执行的测试用例集合在一起就是TestSuite。使用TestLoader来加载TestCase到TestSuite中。
-
TextTestRunner:即测试执行器,用于执行测试用例。该模块中提供run方法执行TestSuite中的测试用例,并返回测试用例的执行结果,如运行的用例总数、用例通过数、用例失败数。
-
report:即测试报告。unittest框架没有自带的用于生成测试报告的模块或接口,需要使用第三方的扩展模块HTMLTestRunner。
3,Unittest 断言
断言在自动化测试脚本中是很重要的内容,只有设置正确合适的断言才能获取正确的测试结果。Unittest框架提供了自己的断言方法,如下:
断言方法 |
判断内容 |
---|---|
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) |
注意:
-
如果断言成功则该条测试用例通过,断言失败则该条测试用例执行失败,且会抛出AssertionError错误。
-
以上提供的断言方法中,都有一个msg参数,默认为None。如果msg参数有对应的值,则断言失败后该msg的值会作为失败信息返回,如 assertEqual(a, b, msg="a与b不相等!") 。
三,Unittest 框架使用方法
1,测试需求
测试对象:构造一个类Math,其中包含整数的加、减法运算。
calculator.py
class Math(): def __init__(self, a, b): self.a = int(a) self.b = int(b) def sum(self): '''和''' return self.a + self.b def sub(self): '''差''' return self.a - self.b
测试需求:对Math类进行单元测试。接下来针对这个测试需求,使用unittest框架编写测试用例。
项目目录结构:后面的例子中,项目结构如下所示。
编辑
2,编写TestCase(测试用例)
在Unittest框架下创建测试用例,步骤如下:
-
1),导入unittest模块。
-
2),创建测试类。测试类的命名不做要求,但需要继承unittest.TestCase类。
-
3),添加setUp()、tearDown()函数,即测试固件。当然还有setUpClass()、tearDownClass() 函数,区别后面会有介绍。
-
4),定义测试方法,即测试用例。测试方法名称必须以test开头,否则测试时该方法将不会被执行。测试方法里需要添加断言。
-
5),调试执行测试用例。执行当前模块的测试用例时,调用unittest.main()方法,该方***搜索该模块下所有以test开头的测试用例方法,并执行。其他方法后面介绍。
针对测试需求,编写测试用例。目录结构如下:
test_sum.py
import unittest from calculator import Math class SumTest(unittest.TestCase): '''测试Math类中的sum函数''' def setUp(self): print("开始执行测试用例{}...".format(self)) def test_sum01(self): m = Math(3, 4) self.assertEqual(m.sum(), 7) def test_sum02(self): m = Math(2, 8) self.assertEqual(m.sum(), 11) def tearDown(self): print("测试用例{}执行结束...".format(self)) if __name__ == '__main__': unittest.main()
运行结果:
.F ====================================================================== FAIL: test_sum02 (__main__.SumTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:/Users/xiaoqq/Desktop/test_project/demo/testSum.py", line 15, in test_sum02 self.assertEqual(m.sum(), 11) AssertionError: 10 != 11 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 开始执行测试用例test_sum01 (__main__.SumTest)... 测试用例test_sum01 (__main__.SumTest)执行结束... 开始执行测试用例test_sum02 (__main__.SumTest)... 测试用例test_sum02 (__main__.SumTest)执行结束... Process finished with exit code 0
结果显示:
-
test_sum01通过,test_sum02失败。点"."表示通过,"F"表示失败。
-
测试类中每个测试方法(即测试用例)执行前,都先执行setUp()方法,每个测试方法执行完毕后都要执行tearDown()方法。
-
断言失败会返回一个AssertionError。
3,在测试用例中添加Fixture(测试夹具)
3.1,测试夹具Fixture的作用对象
在Unittest框架下的测试用例中,使用Fixture有两种方法,作用于两个范围:
-
setUp()、tearDown(),作用于测试方法。即测试类下的每个测试方法执行前先运行setUp(),每个测试方法执行完毕后都要执行tearDown()方法,如testSum.py示例。
-
setUpClass()、tearDownClass(),作用于测试类。即只在整个测试类执行开始时运行setUpClass(),测试类下所有测试方法执行完后运行tearDownClass()。
3.2,setUpClass()、tearDownClass() 举例
test_sum.py修改如下,运行
class SumTest(unittest.TestCase): '''测试Math类中的sum函数''' @classmethod def setUpClass(cls): print("开始执行测试用例{}...".format(cls)) def test_sum01(self): m = Math(3, 4) self.assertEqual(m.sum(), 7) def test_sum02(self): m = Math(2, 8) self.assertEqual(m.sum(), 11) @classmethod def tearDownClass(cls): print("测试用例{}执行结束...".format(cls)) if __name__ == '__main__': unittest.main()
运行结果:
开始执行测试用例<class '__main__.SumTest'>... 测试用例<class '__main__.SumTest'>执行结束... .F
结果显示,setUpClass()、tearDownClass() 都只运行了一次。注意,这里需要使用装饰器@classmethod
3.3,注意
-
在测试用例中,setUp() 或 setUpClass() 做初始化的工作,不是必须有这个函数。同样tearDown() 和 tearDownClass() 只做清理的工作,在测试类中不是必须要有。
-
需要测试的Math类,代码比较简单,没有真正需要用到测试夹具的地方,因此这只是个用法演示。
-
实际自动化过程中,如Web端UI自动化,一般会将创建浏览器实例放在setUp() ,用例执行完后需要关闭浏览器,将关闭浏览器操作放在tearDown()方法里。示例如下:
import unittest from selenium import webdriver class BaiduTest(unittest.TestCase): def setUp(self): '''打开浏览器,进入百度页面''' self.driver = webdriver.Chrome() self.driver.maximize_window() self.driver.get('https://www.baidu.com') def test_01(self): print("操作步骤") def tearDown(self): '''关闭浏览器''' self.driver.quit()
4,将测试用例添加至TestSuite(测试套件)
在testSum.py模块中,使用了unittest.main()方法执行当前模块里的测试用例。除此之外,Unittest还可以通过测试套件构造测试用例集,再执行测试用例。构造TestSuite常用的方法如下:
4.1,方法一:加载测试用例
-
1),先通过unittest.TestSuite() 创建测试套件实例对象。
-
2),再通过addTest() 往测试套件里添加单个测试用例,或通过addTests([...]) 添加多个测试用例(列表中为用例方法名)。
-
3),执行测试套件里的测试用例
run.py示例:
import unittest # 导入测试用例模块 from testcase.test_sum import TestDemo # 第一步:创建TestSuite实例 suite = unittest.TestSuite() # 第二步:将测试用例添加至TestSuite # 方式1,添加单条测试用例 suite.addTest(TestDemo('test_sum01')) # addTest()里参数格式为:测试类('测试方法') suite.addTest(TestDemo('test_sum02')) # 方式2,添加多条测试用例 suite.addTests([TestDemo('test_sum01'), TestDemo('test_sum02')])
4.2,方法二:加载测试用例类
-
1),先通过unittest.TestSuite() 创建测试套件实例对象。
-
2),再通过unittest.TestLoader()创建加载对象,加载测试用例类
run.py示例:
import unittest # 导入测试用例模块 from testcase.test_sum import TestDemo # 创建TestSuite实例 suite = unittest.TestSuite() # 创建一个加载对象 loader = unittest.TestLoader() suite.addTest(loader.loadTestsFromTestCase(TestDemo))
4.3,方法三:加载指定路径里的测试用例
-
1),通过unittest.defaultTestLoader.discover()将指定路径的测试用例加载至测试用例集。注意:这里不需要创建unittest.TestSuite对象
-
2),如下代码所示,test_dir为指定路径。pattern=test_*.py 表示加载以test_开头的模块中的测试用例,指定运行某模块时pattern参数指定全名即可,如:pattern='test_sum.py'。路径跟pattern参数都可以自定义。
run.py示例
import unittest test_dir = './testcase' suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
编写测试项目时,推荐使用方法三。当然还有其他方法,不多做介绍。
5,使用TextTestRunner执行测试用例
unittest框架执行测试用例之前,需先创建TextTestRunner实例,再调用该实例的run()方法。
# 创建TextTestRunner实例 runner = unittest.TextTestRunner() # 使用run()方法运行测试套件(即运行测试套件中的所有用例) runner.run(suite)
run.py修改成如下示例:
import unittest test_dir = './testcase' suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py') runner = unittest.TextTestRunner() runner.run(suite)
运行run.py,结果如下:
.F ====================================================================== FAIL: test_sum02 (test_sum.TestDemo) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\Users\xiaoqq\Desktop\test_project\demo\test_sum.py", line 15, in test_sum02 self.assertEqual(m.sum(), 11) AssertionError: 10 != 11 ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=1) 开始执行测试用例test_sum01 (test_sum.TestDemo)... 测试用例test_sum01 (test_sum.TestDemo)执行结束... 开始执行测试用例test_sum02 (test_sum.TestDemo)... 测试用例test_sum02 (test_sum.TestDemo)执行结束... Process finished with exit code 0
四,输出测试报告
unittest框架执行测试用例完成后会在控制台输出如上的结果,但实际测试过程中,我们需要输出测试报告,这个时候我们需要使用第三方模块。
1,HTMLTestRunner
HTMLTestRunner模块可以直接生成html格式的报告
-
下载地址:HTMLTestRunner - tungwaiyip's software
-
下载后需要修改:
- 94行引入的名称要改,从 import StringIO修改成 import io
- 539行 self.outputBuffer = StringIO.StringIO()修改成self.outputBuffer=io.StringIO()
- 631行 print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)修改成print (sys.stderr, '\nTime Elapsed: %s' %(self.stopTime-self.startTime))
- 642行,if not rmap.has_key(cls): 修改成 if not cls in rmap:
- 766行的uo = o.decode('latin-1'),修改成 uo=o
- 772行,把 ue = e.decode('latin-1') 直接改成 ue = e
-
存放路径:将修改完成的模块存放在Python路径下Lib目录里即可。
run.py示例代码如下:
import time import unittest import HTMLTestRunner # 获取当前时间并指定时间格式,用于测试报告命名 now = time.strftime("%Y-%m-%d_%H_%M_%S") # 测试报告存储路径 report_dir = './report/' # 创建报告文件,并以写的形式打开文件,用于写入报告内容 fp = open(report_dir + now + "_report.html", 'wb') # 初始化一个HTMLTestRunner实例对象,用来生成报告 runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title="App自动化测试报告", description="测试用例情况") # 定义测试用例路径 test_dir='./testcase' # 加载测试用例 suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py') # 执行测试用例 runner.run(suite) fp.close()
运行run.py,会看到report中生成了html文件,即为测试报告
编辑
浏览器打开该文件,内容如下:
编辑
从报告内容中看出HTMLTestRunner.HTMLTestRunner()方法中参数所对应的内容,可以根据项目的实际需要指定参数内容。
2,美化版测试报告
在HTMLTestRunner基础上美化过的报告
-
放置在Python安装路径的Lib文件夹里
run.py示例代码如下:
# -*- coding:utf-8 -*- # @author: 给你一页白纸 import time import unittest import BSTestRunner # 获取当前时间并指定时间格式,用于测试报告命名 now = time.strftime("%Y-%m-%d_%H_%M_%S") # 测试报告存储路径 report_dir = './report/' # 创建报告文件 fp = open(report_dir + now + "_report.html", 'wb') # 初始化一个HTMLTestRunner实例对象,用来生成报告 runner = BSTestRunner.BSTestRunner(stream=fp, title="App自动化测试报告", description="测试用例情况") # 定义测试用例路径 test_dir='./testcase' # 加载测试用例 suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py') # 执行测试用例 runner.run(suite) fp.close()
生成报告样式如下:
编辑
这篇贴子到这里就结束了,最后,希望看这篇帖子的朋友能够有所收获。欢迎留言,或是关注我的专栏和我交流。