- 自动化测试前,需要提前准备好数据,测试完成后,需要自动清理脏数据,有没有更好用的框架?
- 自动化测试中,需要使用多套测试数据实现用例的参数化,有没有更便捷的方式?
- 自动化测试后,需要自动生成优雅、简洁的测试报告,有没有更好的生成方法
pytest介绍与优势
pytest是一个基于Python的自动化测试框架,其实现原理主要包括以下几个部分:
- 1. 支持Python标准库中unittest库的用例风格,同时也支持更为灵活的测试方法定义。
- 2. 使用了丰富的插件机制,可以轻松扩展pytest的功能,如自定义测试收集规则、测试用例执行过程中灵活代理、运行过程中的动态配置等。
- 3. 引入了Fixture机制,再次扩展了pytest框架的功能,Fixture可以理解为测试前提条件、测试数据、测试对象等,在运行测试之前进行准备和清理工作,支持Fixture的各个生命周期钩子函数,实现了依赖注入机制。
相比于其它自动化测试框架,pytest的优势在于:
- 1. 结构清晰,易于扩展和定制,可以直接兼容unittest框架,兼容性强。
- 2. Fixture机制,优化了测试用例编写过程,可以让测试代码更清晰、更简洁、更易于维护。
- 3. 丰富的插件机制,可以轻松扩展pytest的功能,如测试收集机制、测试运行时灵活代理、运行配置等。
- 4. 支持参数化、多进程、分布式处理等并发测试,测试效率高。
总之,pytest框架具有易用、扩展性强、使用文档齐全等优点,适合用于各种规模的测试项目。
- pytest 能够支持简单的单元测试和复杂的功能测试;
- pytest 可以结合 Requests 实现接口测试; 结合 Selenium、Appium 实现自动化功能测试;
- 使用 pytest 结合 Allure 集成到 Jenkins 中可以实现持续集成。
- pytest 支持 315 种以上的插件
1 pytest简介、安装与准备
- 前提:本地已配置完成 Python 环境
- 第一种方式
pip install pytest
进程入cmd界面执行pip install pytest,如果需要指定版本执行pip install pytest=XXX(版本号)
- 第二种方式 PyCharm 直接安装
pycharm中安装可以进入Terminal界面,执行pip install pytest
或者通过pycharm中的settings --->Python Interpreter中进行安装,点击加号进入下面界面
输入pytest,找到pytest对应的第三方库,点击下方的Install Package就可以了
#简单实例,首先创建一个test_sample.py文件,再复制下面的代码到文件中
#功能函数
def func(x):
return x+1
#测试功能函数的用例
def test_case():
assert func(1) ==2
思考:1. 为什么创建的文件名称需要test开头;2. 测试用例函数为什么也是需要test开头,是因为pytest是对文件和用例函数有特定的命名要求,下面我们看看pytest的命名规则
2 pytest命名规则
类型 | 规则 | 实例 |
---|---|---|
文件 | test_开头 或者 _test 结尾 | test_sample.py或者sample_test.py |
类 | Test 开头 | TestDemo |
方法/函数 | test_开头 | test_case() |
注意:测试类中不可以添加__init__ 构造函数,测试包的命名是没有要求的 |
3 pycharm配置与界面化运行
- 进入 Tools->Python Intergrated Tools
- 选择 Default test runner 为 pytest
4 pytest测试用例结构
用例结构3部分组成:用例名称,用例步骤,用例
#用例示例
def test_***(self):
# 测试步骤1
# 测试步骤2
# 断言 实际结果 对比 预期结果
assert ActualResult == ExpectedResult
#类级别的用例示例
class TestXXX:
def setup(self):
# 资源准备
pass
def teardown(self):
# 资源销毁
pass
def test_XXX(self):
# 测试步骤1
# 测试步骤2
# 断言 实际结果 对比 预期结果
assert ActualResult == ExpectedResult
5 pytest测试用例断言
- 什么是断言
断言(assert),是一种在程序中的一阶逻辑(如:一个结果为真或假的逻辑判断式),目的为了表示与验证软件开发者预期的结果。当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息。
2.断言的用法
"""
断言写法
assert <表达式>
assert <表达式>,<描述>
assert <bool expression>;
assert <bool expression> : <message>;
"""
#第一种:assert <表达式>
def test_case():
assert True
def test_c():
a = 1
b = 1
c = 2
assert 'abc' in "abcd"
#第二种:assert <表达式>,<描述>
def test_case1():
a = 1
b = 1
c = 2
assert a + b == c, f"{a}+{b}=={c}, 结果为真"
import sys
def test_plat():
assert ('win32' in sys.platform), "该代码只能在 win32下执行"
6 pytest测试框架结构(setup/teardown)
在接口自动化测试中,用setup方法可以进行测试前的初始化、参数配置等工作,用teardown方法可以进行测试后的清理、还原、退出等工作。pytest测试框架提供了5种类型的setup和teardown的方法,具体如下
类型 | 规则 |
---|---|
setup_module/teardown_module | 全局模块级 |
setup_class/teardown_class | 类级,只在类中前后运行一次 |
setup_function/teardown_function | 函数级,在类外 |
setup_method/teardown_method | 方法级,类中的每个方法执行前后 |
setup/teardown | 在类中,运行在调用方法的前后(重点) |
模块级别的setup和teardown,只执行一次
#模块级别,只被调用一次
def setup_module():
print("资源准备:模块级别setup module")
def teardown_module():
print("资源准备:模块级别teardown module")
#定义函数级别的测试用例
def test_case1():
print('用例01')
#定义函数级别的测试用例
def test_case2():
print('用例02')
函数级别的setup和teardown,每次函数调用的时候,都会调用它们
#定义函数级别的测试用例
def test_case1():
print('用例01')
#定义函数级别的测试用例
def test_case2():
print('用例02')
#函数级别,每次函数调用的时候,都会调用
def setup_function():
print("资源准备:函数级别setup function")
def teardown_function():
print("资源准备:函数级别teardown function")
类级别与方法级别的setup和 teardown
class TestDemo:
#执行类前后分别执行setup_class teardown_class
def setup_class(self):
print("TestDemo setup_class")
def teardown_clase(self):
print("TestDemo teardown_class")
#每个类里面的方法前后分别执行setup,teardown
def setup(self):
print("TestDemo setup")
def teardown(self):
print("TestDemo teardown")
def test_demo1(self):
print("test_demo1")
def test_demo2(self):
print("test_demo2")
7 pytest参数化用例
参数化的目的:
- 通过参数的方式传递数据,从而实现数据和脚本分离。
- 并且可以实现用例的重复生成与执行。
参数化使用场景:
- 测试登录场景
- 测试登录成功,登录失败(账号错误,密码错误)
- 创建多种账号: 中⽂文账号,英⽂文账号
- 普通测试用例方法
- Copy 多份代码 or 读⼊入参数?
- 一次性执⾏多个输⼊入参数
def test_param_login_ok():
# 登录成功
username = "right"
password = "right"
login(username,password)
def test_param_login_fail():
# 登录失败
username = "wrong"
password = "wrong"
login(username,password)
通过pytest 参数化实现方法,装饰器:@pytest.mark.parametrize
#计算两数在【-99,99】之间的和
class Calculator:
def add(self, a, b):
if a > 99 or a < -99 or b > 99 or b < -99:
# print("请输入范围为【-99, 99】的整数或浮点数")
return "参数大小超出范围"
return a + b
ca = Calculator()
#第一个参数对应的是用例的参数,第二个参数对应的参数的value值,第三个参数对应的是用例的名称
@pytest.mark.parametrize(
"a,b",
[[99, 99], [100, 100], [-100, -100], [-99, -99], [0, 59]],
ids=["最大值相加", "100相加", "-100相加", "-99相加", "两数不相同相加"],
)
def test_add(a, b):
data = ca.add(a, b)
if a > 99 or a < -99 or b > 99 or b < -99:
assert data == "参数大小超出范围"
else:
assert data == a + b
#单参数,可以将数据放在列表中
search_list = ['appium','selenium','pytest']
@pytest.mark.parametrize('name',search_list)
def test_search(name):
assert name in search_list
#多参数情况
# 数据放在元组中
@pytest.mark.parametrize("test_input,expected",[
("3+5",8),("2+5",7),("7+5",12)
])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
# 数据放在列表中
@pytest.mark.parametrize("test_input,expected",[
["3+5",8],["2+5",7],["7+5",12]
])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
#笛卡尔积
"""
两组数据
a=[1,2,3]
b=[a,b,c]
对应组合形势
(1,a),(1,b),(1,c)
(2,a),(2,b),(2,c)
(3,a),(3,b),(3,c)
"""
@pytest.mark.parametrize("b",["a","b","c"])
@pytest.mark.parametrize("a",[1,2,3])
def test_param1(a,b):
print(f"笛卡积形式的参数化中 a={a} , b={b}")
对于ids重新对用例命名会出现中文无法显示的情况,那是因为默认的编码格式不是utf-8,需要在项目中建一个conftest.py文件,文件内容如下:
# 创建conftest.py 文件 ,将下面内容添加进去,运行脚本
def pytest_collection_modifyitems(items):
"""
测试用例收集完成时,将收集到的用例名name和用例标识nodeid的中文信息显示在控制台上
"""
for i in items:
i.name=i.name.encode("utf-8").decode("unicode_escape")
i._nodeid=i.nodeid.encode("utf-8").decode("unicode_escape")
8 pytest标记测试用例
- 场景:只执行符合要求的某一部分用例 可以把一个web项目划分多个模块,然后指定模块名称执行。
- 解决: 在测试用例方法上加 @pytest.mark.标签名
- 执行: -m 执行自定义标记的相关用例
pytest -s test_mark_zi_09.py -m=webtest
pytest -s test_mark_zi_09.py -m apptest
pytest -s test_mark_zi_09.py -m "not ios"
import pytest
@pytest.mark.webtest
def test_case():
assert 1 ==1
@pytest.mark.apptest
def test_case1():
assert 'abc' in 'acbccc'
@pytest.mark.iostest
def test_case2():
assert 5+6 > 6
warning是无法识别到iostest的标签,这个没有关系,如果想要pytest识别它们,可以新增一个pytest.ini文件,文件内容如下:
[pytest]
markers = webtest
apptest
iostest
9 pytest设置跳过、预期失败用例
- 这是 pytest 的内置标签,可以处理一些特殊的测试用例,不能成功的测试用例
- skip - 始终跳过该测试用例
- skipif - 遇到特定情况跳过该测试用例
- xfail - 遇到特定情况,产生一个“期望失败”输出
"""
调试时不想运行这个用例
标记无法在某些平台上运行的测试功能
在某些版本中执行,其他版本中跳过
比如:当前的外部资源不可用时跳过
如果测试数据是从数据库中取到的,
连接数据库的功能如果返回结果未成功就跳过,因为执行也都报错
解决 1:添加装饰器
@pytest.mark.skip
@pytest.mark.skipif
解决 2:代码中添加跳过代码
pytest.skip(reason)
xfail 使用场景
与 skip 类似 ,预期结果为 fail ,标记用例为 fail
用法:添加装饰器@pytest.mark.xfail
"""
import sys
import pytest
@pytest.mark.skip(reason="代码未开发完")
def test_aaa():
print("代码未开发完")
assert 1==2
def check_login():
return False
def test_a():
print("start")
if not check_login():
pytest.skip("kkkkkk")
print("end")
@pytest.mark.skipif(sys.platform=='win32', reason='does not run on windows')
def test_case():
assert True
@pytest.mark.skipif(sys.platform=='darwin', reason="does not run on mac")
def test_case1():
assert True
@pytest.mark.skipif(sys.version_info <(3,6), reason="requires python3.6 or higher")
def test_case2():
assert True
@pytest.mark.xfail
def test_case3():
print("test_xfail 方法执行")
assert 1 ==2
xfail = pytest.mark.xfail
@xfail(reason="bug 120")
def test_case4():
assert 1==2
def test_xfail():
print("******开始测试********")
pytest.xfail(reason="该功能尚未完成")
print("测试过程")
assert 1==1
10 pytest运行用例
- 运行 某个/多个 用例包,在Terminal中执行命令pytest -vs可以测试包下所有的用例
- 运行 某个/多个 用例模块:
pytest 文件名.py
- 运行 某个/多个 用例类:pytest 文件名.py::类名
- 运行 某个/多个 用例方法:pytest 文件名.py::类名::方法名
11 pytest测试用例调度与运行
--lf(--last-failed)
只重新运行故障。
-
--ff(--failed-first)
先运行故障然后再运行其余的测试
12 pytest命令行常用参数
—help 可以查看pytest所有的命令参数
-x 用例一旦失败(fail/error),就立刻停止执行
--maxfail=num 用例达到
-m 标记用例
-k 执行包含某个关键字的测试用例
-v 打印详细日志
-s 打印输出日志(一般-vs一块儿使用)
—collect-only(测试平台,pytest 自动导入功能 )
13 python执行pytest
- 使用 main 函数
if __name__ == '__main__':
# 1、运行当前目录下所有符合规则的用例,包括子目录(test_*.py 和 *_test.py)
pytest.main()
# 2、运行test_mark1.py::test_dkej模块中的某一条用例
pytest.main(['test_mark1.py::test_dkej','-vs'])
# 3、运行某个 标签
pytest.main(['test_mark1.py','-vs','-m','dkej'])
运行方式
`python test_*.py `
- 使用 python -m pytest 调用 pytest(jenkins 持续集成用到)
14 pytest异常处理
- 可以捕获特定的异常
- 获取捕获的异常的细节(异常类型,异常信息)
- 发生异常,后面的代码将不会被执行
def test_raise():
with pytest.raises(ValueError):
raise ValueError("value msut be 0 or None")
def test_raise1():
with pytest.raises(ValueError) as exc_info:
raise ValueError("value must be 42")
assert exc_info.type is ValueError
assert exc_info.value.args[0] == 'value must be 42'