pytest
成熟的全功能 Python 测试框架
- 简略灵便,容易上手
- 反对参数化
- 测试用例的 skip 与 xfail,主动失败重试等解决
- 可能反对简略的单元测试和简单的功能测试,还能够用来做 selenium/appium 等自动化测试、接口自动化测试(pytest+requests)
- 具备很多第三方插件,并能够自定义扩大:pytest-allure,pytest-xdist(多 CPU 开发)等
- 反对 Jenkins 集成
Pytest 使用手册
pytest 库装置:
pip install pytest
测试用例的辨认与运行
- 对于文件的命名要求 :
-
- test_*.py
- *_test.py
- 对于用例的命名要求:
-
- Test 类蕴含的所有 test_ 的办法(测试类不能带有 _ init_ 办法)
- 不在 class 中的所有的 test_* 办法
pytest 能够执行 unittest 框架些的用例和办法
pycharm 应用 pytest 框架时,须要指定,具体操作如图所示:
命令行运行形式:
# 间接执行以后所在门路下辨认到的测试用例
pytest
# 执行指定文件
pytest test_material_master.py
# 执行时打印日志
pytest -v
复原应用 Python 解释器运行的形式:
此时的运行形式为:
import pytest
if __name__ == '__main__':
pytest.main(["test_material_master.py"]) # 运行整个文件的用例
pytest.main(["test_material_master.py::TestMaterialMaster::test_mat_search_by_matCode"]) # 运行文件中指定的某条的用例
参数化应用
应用办法:
# 作为装璜器
@pytest.mark.parametrize(argnames, argvalues)
# argnames:要参数化的变量;类型:能够是 str(应用逗号宰割),list,tuple
# argvalues:参数化的值;类型:list,[tuple]
理论利用:
@pytest.mark.parametrize("a, b, expect",
get_data(yaml_path, "add"),
ids=["整数", "小数", "大整数"])
def test_add(self, a, b, expect, setup_fixture):
"""测试加法正向用例"""
result = setup_fixture.add_func(a, b)
assert abs(result - expect) < 0.01
注意事项:
参数组合(笛卡尔积):实用于只有一个冀望后果的状况
import pytest
@pytest.mark.parametrize("a", [1, 2, 3])
@pytest.mark.parametrize("b", [4, 5, 6])
def test_add(a, b):
print("参数组合 a = {}, b = {}".format(a, b))
执行后果:PASSED [11%] 参数组合 a = 1, b = 4
PASSED [22%] 参数组合 a = 2, b = 4
PASSED [33%] 参数组合 a = 3, b = 4
PASSED [44%] 参数组合 a = 1, b = 5
PASSED [55%] 参数组合 a = 2, b = 5
PASSED [66%] 参数组合 a = 3, b = 5
PASSED [77%] 参数组合 a = 1, b = 6
PASSED [88%] 参数组合 a = 2, b = 6
PASSED [100%] 参数组合 a = 3, b = 6
skip
应用场景:写测试用例时,发现某个用例自身就存在 bug,而且临时无奈修复,就能够先跳过它
import pytest
@pytest.mark.skip("存在 bug,先跳过")
@pytest.mark.parametrize("a", [1, 2, 3])
@pytest.mark.parametrize("b", [4, 5, 6])
def test_add(a, b):
print("参数组合 a = {}, b = {}".format(a, b))
mark
应用场景: 对用例进行分类,贴标签
import pytest
@pytest.mark.test1
def test_01():
print("标记为冒烟测试用例")
@pytest.mark.test2
def test_02():
print("标记为回归测试用例")
# 只执行标记了 test1 的用例,命令行:# pytest -s test.py -m test1
# 反选 pytest -s test.py -m "not test1"
前置与后置
用例运行级别
- 模块级:开始与模块始末,全局无效;
setup_module / teardown_module
- 函数级:只对函数用例失效(不在类中应用);
setup_function / teardown_function
- 类级:只在类的前后运行一次(在类中应用);
setup_class / teardown_class
- 办法级:开始与办法始末(在类中应用);
setup_method / teardown_method
- 类中应用,运行在调用办法的前后;
setup / teardown
最为罕用 **
fixture:自定义前置 / 后置
@pytest.fixture() 的劣势:
- 命名形式灵便,不局限于 setup / teardown
- 通过 conftest.py 配置能够实现数据共享,不须要 import 就能自动识别
scope="module"
能够实现多个 .py 跨文件共享前置,每个 .py 文件调用一次scope="session"
能够实现多个 .py 跨文件应用一个 session 来实现多个用例
import pytest
@pytest.fixture(scope="function") # 通过配置装璜器定义
def login_fixture():
"""登陆前置"""
print("提前登陆")
def test_01(login_fixture): # login_fixture 作为参数传入
print("测试用例 01")
def test_02():
print("测试用例 02")
执行后果:test.py::test_01 提前登陆 PASSED [50%] 测试用例 01
test.py::test_02 PASSED [100%] 测试用例 02
fixture 参数
scope:作用范畴
- function:每个 test 都运行,默认是 function 的 scope
- class:每个 class 的所有 test 只运行一次
- module:每个 module 的所有 test 只运行一次
- session:每个 session 只运行一次
autouse: 审慎应用
- 默认为 False
- 当为 True,每个测试用例会主动调用该 fixture,无需传入 fixture 函数名
fixture 中应用参数
fixture 函数能够参数化,在这种状况下,它们将被屡次调用,每次执行一组相干测试,即依赖于这个 fixture 的测试,测试函数通常不须要晓得它们的从新运行
import pytest
@pytest.fixture(params=["参数 1","参数 2"])
def myfixture(request):
print("执行 testPytest 里的前置函数,%s" % request.param)
fixture 中返回参数
import pytest
@pytest.fixture(params=["参数 1","参数 2"])
def myfixture(request):
return request.param
def test_print_param(myfixture):
print("执行 test_two")
print(myfixture)
assert 1==1
# 输入
PASSED [50%] 执行 test_two
参数 1
PASSED [100%] 执行 test_two
参数 2
conftest.py
利用场景:
- 每个接口需共用到的 token
- 每个接口需共用到的测试用例数据
- 每个接口需共用到的配置信息
应用的注意事项:
- 文件名 conftest.py 是固定的, 不能批改为其余文件名
- conftest.py 文件与运行的用例要在同一个 pakage 下,并且有 __init__.py 文件
- 不须要通过 import 导入,pytest 的用例会自动识别
- 所有同目录测试文件运行前都会执行 conftest.py 文件
# conftest.py 文件
import pytest
@pytest.fixture()
def login():
print("登陆前置")
后置操作 -yield
@pytest.fixture(scope="function")
def demo_fix():
print("测试用例的前置筹备操作")
yield
print("测试用例的后置操作")
def test_1(demo_fix):
print("开始执行测试用例 1")
def test_2():
print("开始执行测试用例 2")
def test_3(demo_fix):
print("开始执行测试用例 3")
如果测试用例中的代码出现异常或者断言失败,并不会影响他的固件中 yield 后的代码执行;
然而如果 fixture 中的 yield 之前的代码也就是相当于 setup 局部的带代码,呈现谬误或断言失败,那么 yield 后的代码将不会再执行,当然测试用例中的代码也不会执行
终结函数 -addfinalizer
相当于 try…except 中的 finally
即便 setup 呈现问题了,addfinalizer 仍会执行 teardown 的操作
@pytest.fixture(scope="session")
def login_xadmin_fix(request):
s = requests.session()
login_xadmin(s)
def close_s():
s.close() # 敞开 s 用例实现后最初的清理
request.addfinalizer(close_s)
return s
罕用参数阐明
- -v:能够输入用例执行的详细信息;如用例坐在的文件及用例名称
- -s:输出用例的调试信息;如 print 的打印信息
- -x:遇到失败的用例时立刻进行
- -maxfail:用例失败达到肯定数量时,进行运行;
pytest -maxfail=num
- -m:运行含有
@pytest.mark. 标记名
的测试用例 - -k:执行合乎匹配的测试用例测试;如
pytest -k "raises and not delete"
运行所有蕴含 raises 但不蕴含 delete 的测试
pytest 实用插件介绍
pytest-rerunfailures:用例失败后主动从新运行
装置办法:
pip install pytest-rerunfailures
应用办法:
pytest test_x.py --reruns=n #失败后重运行的次数
同时也能够在脚本中指定定义重跑的次数,这个时候在运行的时候,就无需加上 –reruns 这个参数
@pytest.mark.flaky(reruns=6, reruns_delay=2)
def test_example(self):
print(3)
assert random.choice([True, False])
pytest-assume:多重校验
pytest 中的 python 的 assert 断言,也能够写多个断言,但一个失败,前面的断言将不再执行
而 pytest-assume,即便后面的断言失败了,后续的断言也会继续执行
装置办法:
pip install pytest-assume
应用办法:
def test_simple_assume(x, y):
pytest.assume(x == y)
pytest.assume(True)
pytest.assume(False)
pytest-xdist:分布式并发执行
pytest-xdist 能够让自动化测试用例能够分布式执行,从而节俭自动化测试工夫
分布式执行用例的设计准则:
- 用例之间是独立的,用例之间没有依赖关系,用例能够齐全独立运行【独立运行】
- 用例执行没有程序,随机程序都能失常执行【随机执行】
- 每个用例都能反复运行,运行后果不会影响其余用例【不影响其余用例】
装置办法:
pip install pytest-xdist
应用办法:
多 CPU 并行执行用例,间接加个 -n 参数即可,前面 num 参数就是并行数量,比方 num 设置为 3
pytest -n 3
pytest-ordering:管制用例的执行程序
装置办法:
pip install pytest-ordering
应用办法:
import pytest
@pytest.mark.run(order=2)
def test_foo():
assert True
@pytest.mark.run(order=1)
def test_bar():
assert True
PS: 尽量不要让测试用例有程序,尽量不要让测试用例有依赖!
hook(钩子)函数定制和扩大插件
Pytest 在收集完所有测试用例后调用该钩子办法。咱们能够定制化性能实现:
- 自定义用例执行程序
- 解决编码问题(中文测试用例名称)
- 主动增加标签
倡议将 hook 函数的代码写在 conftest.py 文件中
# conftest.py
from typing import List
def pytest_collection_modifyitems(session: "Session", config: "Config", items: List["Item"]
) -> None:
for item in items:
item.name = item.name.encode("utf-8").decode("unicode-escape")
item._nodeid = item.nodeid.encode("utf-8").decode("unicode-escape")