目录
1、unittest核心要素
(1)TestCase:测试用例
TestCase继承于 unittest.TestCase
测试方法必须以test开头
(2)TestSuit:测试套件
说明:测试套件是把多条测试用例集合在一起,就是一个TestSuite;
1: 实例化测试执行器: runner = unittest.TextTestRunner()
2: 执行测试套件 : runner.run(suite)
(3)TextTestRunner
说明:测试执行是用来执行测试用例套件
实例化 runner 对象 runner = unittest.TextTestRunner()
执行测试套件 runner.run(suite)
(4)FixTure:概述:
测试用例类中实现了前置和后置方法,则这个测试类就是一个Fixture。
例如实现了: setUp() 和 tearDown
拓展:defaultTestLoader
说明: unittest.defaultTestLoader.discover 方法可以自动把指定目录下测试用例自动添加到测试套件中,避免手动一个个的添加测试用例。这个方法返回的就是 suite 对象。
①指定要搜索的目录
②调用 discover 方法生成 suite 对象
流程图如下:
代码演示:
# 1: 导包
import unittest
# 2: 准备测试类
class MyTest(unittest.TestCase):
def test_1(self):
print("这是:test_1")
def test_2(self):
print("这是:test_2")
if __name__ == '__main__':
# 3: 将测试用例添加到 测试套件中。
# 实例化测试套件
suite = unittest.TestSuite()
# 将测试用例添加到测试套件中。
suite.addTest(MyTest('test_1'))
suite.addTest(MyTest('test_2'))
# 4:运行容器中的测试用例
# 实例化测试执行器
runner = unittest.TextTestRunner()
runner.run(suite)
拓展defaultTestLoader代码演示:
2、Unittest的使用
- unittest使用步骤
使用unittest编写python的单元测试代码,包括如下几个步骤:
1、导入unittest模块
2、定义一个继承自unittest.TestCase的测试用例类,如class xxx(unittest.TestCase):
3、定义setUp和tearDown,如果定义了则会在每个测试case执行前先执行setUp方法,执行完毕后执行tearDown方法。
4、定义测试用例,名字以test开头,unittest会自动将test开头的方法放入测试用例集中。
5、一个测试用例应该只测试一个方面,测试目的和测试内容应很明确。主要是调用assertEqual、assertRaises等断言方法判断程序执行结果和预期值是否相符。
6、调用unittest.main()启动测试,或实例化runner对象执行测试集、测试函数
7、如果测试未通过,则会显示e,并给出具体的错误(此处为程序问题导致)。如果测试失败则显示为f,测试通过为.,如有多个testcase,则结果依次显示。
注意:
- unittest默认加载脚本(测试用例)的顺序是:根据测试用例的名称,按照ASCII码的顺序加载,数字与字母的顺序为:0-9,A-Z,a-z
3、断言
前言:
自动化脚本在执行的时候一般都是无人值守状态,我们不知道执行结果是否符合预期结果,所以我们需要让程序代替人为检测程序执行的结果是否符合预期结果,这就需要使用断言。
# arg1和arg2相等,测试通过,标志为 .
# arg1和arg2不相等,测试不通过,标志为 F,抛出异常,显示msg的信息
unittest.assertEqual(arg1, arg2, msg=None)
4、unittest参数化
首先,在pycharm终端安装包
pip install parameterized -i https://pypi.tuna.tsinghua.edu.cn/simple
# 1. 参数化参数:列表套元组
# 2. 列表有几个元素,测试用来执行几次
# 3. 元组元素的位置匹配测试用例的形参
代码演示:
import unittest
from parameterized import parameterized
class MyTest(unittest.TestCase):
def setUp(self) -> None:
print('setUp')
def tearDown(self) -> None:
print('tearDown')
@parameterized.expand([('mike', '123'), ('yoyo', 'abc')])
def test_params(self, name, pwd):
print('name = %s, pwd = %s'%(name, pwd))
if __name__ == '__main__':
unittest.main()
结果:循环两次
5、Mock 类基本使用
5.1、return_value的使用
import unittest
import unittest.mock
class MyTest(unittest.TestCase):
def test_return(self):
# 1. 创建Mock()对象,给return_value关键字传参
mock_obj = unittest.mock.Mock(return_value=250)
# mock_obj是对象,可调用对象,用法和函数一样
ret = mock_obj()
print(ret)
5.2、side_effect的使用:
1、利用side_effect传递一个异常:
mock_obj = unittest.mock.Mock(side_effect=BaseException(“自定义异常”))
2、使用side_effect传递一个数组:
mock_obj = unittest.mock.Mock(side_effect=[1, 2, 3])
3: 使用side_effect传递一个函数:
mock_obj = unittest.mock.Mock(side_effect=function)
利用side_effect传递一个异常示例:
# 1:导包
import unittest
import unittest.mock
# 2: 创建模拟的类
class MyTest(unittest.TestCase):
def test_return(self):
# 创建一个能返回250的可调用对象
mock_obj = unittest.mock.Mock(side_effect=BaseException("自定义异常"))
# 调用mocke对象,拿到返回值
ret = mock_obj()
print(ret)
if __name__ == '__main__':
unittest.main()
结果:
5.3、mock案例
业务需求: 现在有一个需求: A
负责开发支付
功能(未完成)。B
负责支付状态
模块(完成),B代码中需要调用A写的支付函数。现在要求测试人员C
测试B写的代码是否符合要求,C如何测试??
解决方法: 使用mock替换A写的函数,然后调用B写的函数的时候就会调用我们的mock,不会调用A未完成的代码。
流程详解:
- 注意: mock的副作用
当我们使用 mock 掉一个对象后,默认情况下,在后面执行的代码都会受到影响,比如其他的测试用例,只要是在 mock 之后执行的都会受影响。
比如:前面测试人员C写的的测试用例test_success
使用了mock,替换了A的返回值,那么再写一个测试用例test_success2
,即使没有mock对象,也会得到前面mock之后的结果:
import unittest
import unittest.mock
import pay
import pay_status
class TestPay(unittest.TestCase):
def test_success(self):
# 用一个mock来代替pay中的pay_way函数
pay.pay_way = unittest.mock.Mock(return_value={"result": "success", "reason":"null"})
# 盗用要测试的函数
ret = pay_status.pay_way_status()
self.assertEqual(ret, '支付成功', '支付失败')
#测试用例2
def test_success2(self):
# 盗用要测试的函数
ret = pay_status.pay_way_status()
self.assertEqual(ret, '支付成功', '支付失败')
if __name__ == '__main__':
unittest.main()
终端测试结果:
-
我们发现,两个测试用例的结果相同,测试用例
test_success2
,即使没有mock对象,也会得到前面mock之后的结果。那么如果我只想某一个测试用例生效,另外的不生效,该如何解决呢?? -
答:使用patch装饰器、或patch上下文管理器
5.4、限制mock的生效范围
(1)patch装饰器:
- 1:在测试用例上面加上装饰器:
@mock.patch(‘被代替的函数路径’)
- 2: 测试用例增加一个
传参
。 - 3: 测试用例内部直接使用 :
传入值的名字.return_value = {返回值键值对}
import unittest
from unittest import mock
import pay
import pay_status
class TestPay(unittest.TestCase):
# 1、装饰器
@mock.patch('pay.pay_way')
def test_success(self, mock_pay_way): # 2、传参
# 3、传入值的名字.return_value = {返回值键值对}
mock_pay_way.return_value={"result": "success", "reason":"null"}
# 盗用要测试的函数
ret = pay_status.pay_way_status()
self.assertEqual(ret, '支付成功', '支付失败')
if __name__ == '__main__':
unittest.main()
(2) patch上下文管理器:
- 1: 函数内部使用 :·
with mock.patch(‘被替代的函数路径’) as 别名:
- 2:
别名.return_value = {返回值键值对}
import unittest
from unittest import mock
import pay
import pay_status
class TestPay(unittest.TestCase):
def test_success(self):
with mock.patch('pay.pay_way') as mock_pay_way:
# 用一个mock来代替pay中的pay_way函数
mock_pay_way.return_value={"result": "success", "reason":"null"}
# 盗用要测试的函数
ret = pay_status.pay_way_status()
self.assertEqual(ret, '支付成功', '支付失败')
if __name__ == '__main__':
unittest.main()
5.5、类方法替换
刚刚上面被替换的都是一些函数,如果想要mock替换一个类中的某个函数如何操作呢?
- 待测试代码:
from unittest import mock
import unittest
class Pay(object):
def pay_way(self):
"""假设这里是一个支付的功能,未开发完
支付成功返回:{"result": "success", "reason":"null"}
支付失败返回:{"result": "fail", "reason":"余额不足"}
reason返回失败原因
"""
raise NotImplementedError('代码还没有实现')
def pay_way_status(self):
"""根据支付的结果success或fail,判断跳转到对应页面
假设这里的功能已经开发完成"""
# todo 此时pay_way()函数并未完成!你先假定他完成了
result = self.pay_way()
print(result)
if result["result"] == "success":
return "支付成功"
if result["result"] == "fail":
return "支付失败"
方案一: 传统方式
1: 先实例化这个类
2:对象.被替换函数 = mock.Mock(return_value = {返回的键值对})
3:对象.被测函数()
class TestPayStatues(unittest.TestCase):
'''单元测试用例'''
def test_success1(self):
'''测试支付成功场景'''
p = Pay() # 实例化对象
p.pay_way = mock.Mock(return_value = {"result": "success", "reason":"null"})
statues = p.pay_way_status()
print(statues)
self.assertEqual(statues, "支付成功")
方案二: 装饰器
1: 在测试函数上增加装饰器:@mock.patch.object(类, ‘被代替的方法名’)
2: 在增加一个函数传值,def test_success2(self, mock_obj):
3: 使用新传值.return_value = {返回的键值对}
4: 实例化类.被测试函数()
class TestPayStatues(unittest.TestCase):
@mock.patch.object(Pay, 'pay_way')
def test_success2(self, mock_obj):
'''测试支付成功场景'''
mock_obj.return_value = {"result": "success", "reason":"null"}
# 根据支付结果测试页面跳转
statues = Pay().pay_way_status()
print(statues)
self.assertEqual(statues, "支付成功")
方案三:使用whith上下文
1: with mock.patch.object(类, ‘被代替的方法名’) as 新名字
2:新名字.return_value = {}
3: 实例化类.被测试函数()
class TestPayStatues(unittest.TestCase):
def test_success3(self):
'''测试支付成功场景'''
with mock.patch.object(Pay, 'pay_way') as mock_obj:
mock_obj.return_value = {"result": "success", "reason":"null"}
# 根据支付结果测试页面跳转
statues = Pay().pay_way_status()
print(statues)
self.assertEqual(statues, "支付成功")
5.6、mock的常用的两个属性:
- mock.called: 决定mock是否被调用过。
- mock.call_count : 决定mock被调用的次数。
import unittest
import unittest.mock
class MockTest(unittest.TestCase):
def test_return_value(self):
mock_obj = unittest.mock.Mock(return_value=1999)
result = mock_obj()
print(result) # 打印 1999
print(mock_obj.called) # 是否被调用过, 返回布尔值
print(mock_obj.call_count) # 获取调用测试, 返回调用测试
if __name__ == '__main__':
unittest.main()
6、生成测试报告
(1)HTMLTestRunner
# 安装包
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple htmltestrunner-python3
。。。。
测试代码(略)
。。。。
if __name__ == '__main__':
# 1. 把测试用例添加到suite套件中
suite = unittest.defaultTestLoader.discover('./', 'test_1_html.py')
# 2. 打开文件,是一个文件对象
with open('./HTMLTestRunner.html', 'w', encoding='utf-8') as f:
# 3. HTMLTestRunner()创建一个runner对象
runner = HTMLTestRunner(
stream=f, # 测试报告需要写入到的文件
verbosity=2, # 控制台输出信息的详细程度, 默认为1
title='这是报告标题', # 测试报告的标题
description='这是一个测试报告' # 测试报告的描述
)
# 4. runner把容器中测试用例运行
runner.run(suite)
(2)BeautifulReport
# 安装包
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple beautifulreport
。。。
测试代码(略)
。。。
if __name__ == '__main__':
# 1. 把测试用例添加到suite容器中
suite = unittest.defaultTestLoader.discover('./', 'test_2_beautiful.py')
# 2. 创建runner对象,同时把suite传参进入
runner = BeautifulReport(suite)
# 3. 运行,同时生成测试报告
# 参数1:生成文件的注释, 参数2:生成文件的filename, 参数3:生成report的文件存储路径
runner.report('报告描述必须有,在报告中显示为用例名称', '测试报告文件名', './')