文章目录
前言
阅读本文前请注意最后编辑时间,文章内容可能与目前最新的技术发展情况相去甚远。欢迎各位评论与私信,指出错误或是进行交流等。
什么是单元测试
单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证。 单元测试中单元的含义,单元就是人为规定的最小的被测功能模块,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。
什么是单元测试框架
理论上来说,不使用单元测试框架也能进行单元测试。但如果用于单元测试的代码(即测试用例)增多,在没有测试框架的情况下会变得拥挤、不可管理,这个时候引入测试框架就变得尤为重要。
单元测试框架提供了一种统一的编程模型,可以将测试定义为一些简单的类,这些类中的方法可以调用希望测试的应用程序代码。利用单元测试框架,可以很轻松地插入、设置和分解有关测试的功能,可以直观方便地管理测试用例。
主流的单元测试框架,如Java的Junit、python的Unittest、Pytest
单元测试框架作用:
- 提供用例组织与执行
- 提供丰富的断言方法
- 提供丰富的日志与测试结果
Unittest 测试框架
Unittest是Python自带的单元测试框架,不仅适用于单元测试,还可用于Web、Appium、接口自动化测试用例的开发与执行。该测试框架可组织执行测试用例,并且提供丰富的断言方法,判断测试用例是否通过,并最终生成测试结果。
Unittest官方文档:unittest — Unit testing framework — Python 3.10.4 documentation
Unittest核心要素
TestCase
即测试用例,Unittest提供testCase类来编写测试用例,一个TestCase的实例就是一个测试用例。一条测试用例就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown),通过运行一条测试用例,可以对某一个问题进行验证。
Fixture
即测试固件,用于测试用例环境的搭建和销毁。在测试步骤执行前需要为该测试用例准备环境(SetUp),如启动app或打开浏览器,测试步骤执行后需要恢复环境 (TearDown),如关闭app或浏览器,这时候就需要用到Fixture,使代码更简洁。
TestSuite
即测试套件,把需要执行的测试用例集合在一起就是TestSuite。使用TestLoader来加载TestCase到TestSuite中。
TextTestRunner
即测试执行器,用于执行测试套件。该模块中提供run方法执行TestSuite中的测试用例,并返回测试用例的执行结果,如运行的用例总数、用例通过数、用例失败数。
report
即测试报告。unittest框架没有自带的用于生成测试报告的模块或接口,需要使用第三方的扩展模块。
Unittest 断言
补充:
如果断言成功则该条测试用例通过,断言失败则该条测试用例执行失败,且会抛出AssertionError错误。
以上提供的断言方法中,都有一个msg参数,默认为None。如果msg参数有对应的值,则断言失败后该msg的值会作为失败信息返回,如 assertEqual(a, b, msg=“a与b不相等!”) 。
Unittest 框架使用
测试对象:构造一个类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框架编写测试用例。
编写TestCase(测试用例)
在Unittest框架下创建测试用例,步骤如下:
- 1),导入unittest模块。
- 2),创建测试类。测试类的命名不做要求,但需要继承unittest.TestCase类。
- 3)添加setUp()、tearDown()函数,即测试固件。(非必须)
- 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"表示失败。
- 断言失败会返回一个AssertionError。
在测试用例中添加Fixture(测试夹具)
在Unittest框架下的测试用例中,使用Fixture有三种方法:
setUp()、tearDown(),作用于测试方法。即测试类下的每个测试方法执行前先运行setUp(),每个测试方法执行完毕后都要执行tearDown()方法,如testSum.py示例。
setUpClass()、tearDownClass(),作用于测试类。即只在整个测试类执行开始时运行setUpClass(),测试类下所有测试方法执行完后运行tearDownClass()。且需要使用装饰器@classmethod
setUpModule()、tearDownModule(),作用于测试模块(可以理解为整个TestCase.py文件)。即只在整个测试模块执行开始时运行setUpModule(),测试类下所有测试模块执行完后运行tearDownModule()。
以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
注意
- 在测试用例中,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()
将测试用例添加至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')])
TestLoader介绍
下文方法二和方法三都是TestLoader的使用
方法二:加载测试用例类
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))
方法三:加载指定路径里的测试用例
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')
使用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框架执行测试用例完成后会在控制台输出如上的结果,但实际测试过程中,我们需要输出测试报告,这个时候我们需要使用第三方模块。例如 HTMLTestRunner,如果对该模块使用有需求可查看该博客软件测试——Unittest单元测试框架详解,或者查看该模块的官方文档。
本文介绍的是另外的一个第三方模块,unittestreport。
UnitTestReport 是一个基于 Python 的开源库,专为改进 unittest 框架的测试报告体验而生。它不仅解决了标准 unittest 报告样式单一且不够直观的问题,还添加了许多实用特性,如生成丰富的 HTML 测试报告、支持数据驱动测试、测试用例的失败重试、多线程并发执行、以及自动化测试结果的通知功能,如发送至邮箱或企业协作平台如钉钉和企业微信。
该项目由木森发起并维护,旨在通过提供详细且易于理解的测试报告,提升软件开发过程中的测试质量和效率。无论是小型项目还是大型企业级应用的测试流程,UnitTestReport都能助力团队更有效地管理和分析测试结果。
github地址:UnitTestReport
使用文档:UnitTestReport使用文档
目前UnitTestReport实现了以下功能:
- HTML测试报告生成
- unittest数据驱动
- 测试用例失败重运行
- 多线程并发执行用例
- 发送测试结果及报告到邮箱
- 测试结果推送到钉钉
- 测试结构推送到企业微信
- 使用pytest生成unittestreport的html报告(最新)
报告样式展示:
至于具体用法大家参考官方使用文档即可,文档中都已经给出了详细的用法、例子、解释。
跳过(补充)
代码
import unittest
version = 30
class TestDemo(unittest.TestCase):
# 使用skip装饰器表示跳过
@unittest.skip('没什么原因')
def test_1(self):
print('测试方法1')
@unittest.skipIf(version >= 30, '版本大于等于30,不用测试')
def test_2(self):
print('测试方法2')
def test_3(self):
print('测试方法3')
运行结果
可以发现,跳过了测试方法1,并根据判断跳过了不满足条件的测试方法2.
参数化
定义
parameterized模块的实现步骤
首先使用 pip install parameterized 安装参数化插件。
parameterized 包的使用如下图所示
ddt模块的实现步骤
我们设计测试用例时,会出现测试步骤一样,只是其中的测试数据有变化的情况,比如测试登录时的账号密码。这个时候,如果我们依然使用一条case一个方法的话,会出现大量的代码冗余,而且效率也会大大降低。此时,ddt模块就能帮助我们解决这个问题。
ddt(data-driven test),顾名思义,数据驱动测试。这个模块是第三方库,需要我们自己下载。或者直接在命令行输入pip install ddt。
使用方法:
- 使用数据驱动,要在class前加上修饰器 @ddt
import unittest
from ddt import ddt, data
@ddt
class TestDemo(unittest.TestCase):
# 单一参数
@data('17611110000', '17611112222')
def test_1(self, phone):
print('测试一电话号码:', phone)
if __name__ == '__main__':
unittest.main()
else:
pass
- 在实际中不一定是单一参数进行传参,将会使用多个参数进行传参:
注意事项:
1)、多个数据传参的时候@data里面是要用列表形式
2)、会用到 @unpack 装饰器 进行拆包,把对应的内容传入对应的参数;
import unittest
from ddt import ddt, data, unpack
@ddt
class TestDemo(unittest.TestCase):
# 多参数数据驱动
@data(['admin', '123456'])
# unpack 是进行拆包,不然会把列表里面的数据全部传到username这个一个参数,我们要实现列表中的两个数据分别传入对应的变量中
@unpack
def test_2(self, username, password):
print('测试二:', username, password)
if __name__ == '__main__':
unittest.main()
else:
pass
但是以上步骤都是数据在代码当中的,假如要测试n个手机号这样的数据,全部写在 @data 装饰器里面就很麻烦,这就引出了数据驱动里面的代码和数据的分离。
将数据放入一个文本文件中,从文件读取数据, 如JSON、 excel、 xml、 txt等格式文件
- json文件数据
[
{
"username": "admin",
"password": "123456"
},
{
"username": "normal",
"password": "45678"
}
]
在测试代码中读取json文件
import json
import unittest
from ddt import ddt, data, unpack
# 用json多个参数读取
def reads_phone():
with open('user.json', encoding='utf-8') as f:
result = json.load(f) # 列表
return result
@ddt
class TestDemo(unittest.TestCase):
# 多参数数据驱动
@data(*reads_phone())
# unpack 是进行拆包,不然会把列表里面的数据全部传到username这个一个参数,我们要实现列表中的两个数据分别传入对应的变量中
@unpack
def test_2(self, username, password):
print('测试二:', username, password)
if __name__ == '__main__':
unittest.main()
else:
pass
注意事项:
1、with open里面默认是 ”r“
2、@data 里面的 * 含义是实现每个json对象单个传入方法执行,不然会吧json文件里面所用数据全部传入
> * 是元祖;
> ** 是字典;
3、参数不能传错,要对应
执行结果:
- txt文件驱动, 一行表示一组数据:
admin,123456
normal,456789
import unittest
def read():
lis = []
with open('readtext.txt', 'r', encoding='utf-8') as f:
for line in f.readlines():
# lis.append(line) # ['admin,123456\n', 'normal,456789\n']
# lis.append(line.strip('\n')) ['admin,123456', 'normal,456789'] 两个字符串
lis.append(line.strip('\n').split(',')) # [['admin', '123456'], ['normal', '456789']]
return lis
class TestDome(unittest.TestCase):
def test_01(self):
li = read()
print(li)
if __name__ == '__main__':
unittest.main()
剩余的包括CSV、yaml等文件类型操作 可以参考这篇博客自动化测试之unittest框架详解
参考目录
https://blog.csdn.net/qaqqqqqaq_/article/details/135389040
https://blog.csdn.net/Asaasa1/article/details/109402334
https://blog.csdn.net/Bala_lala/article/details/123362849
https://blog.csdn.net/HUA1211/article/details/136660873
https://www.bilibili.com/video/BV11g411V7Kf
https://www.bilibili.com/video/BV12b4y1J7H6
https://www.bilibili.com/video/BV19M4y147i6
https://www.bilibili.com/video/BV1TP411u7Ti