大家好,我是玉米君,本篇文章将从基础、断言、夹具、标记、配置、插件和布局几个方面带大家了解pytest测试框架,并配置脚本案例,让你熟悉起来更方便。
1. pytest特点和基本用法
Python内置了测试框架unit test,但是了解units同学知道它是一个拥有浓烈的Java风格,比如说类名、方法名字会使用驼峰,而且必须要继承父类才能的定义测试用例等等。
那有一些Python开发者,他觉得这种方式这种风格不太适应,所以做了一个更加pythonic的测试框架,最开始只是工具箱的一部分(py.test),后来这个测试框架独立出来的就成为了大名鼎鼎的pytest。
1.1 安装pytest
使用pip进行安装
pip install pytest -U
验证安装
pytest
pytest --version
pytest -h
1.2 创建测试用例
- 创建
开头的python文件test_
- 编写
开头的函数test_
- 在函数中使用
关键字assert
# test_main.py
def test_sanmu():
a = 1
b = 2
assert a == b
1.3 执行测试用例
- 自动执行所有的用例
-
pytest
-
- 执行指定文件中所有用例
-
pytest filename.py
-
- 执行指定文件夹中的所有文件中的所有用例
-
pyest dirname
-
- 执行指定的用例
-
pytest test_a.py::test_b
-
测试发现:搜集用例
一般规则:
- 从当前目录开始,遍历每一个子目录 (不论这个目录是不是包)
- 在目录搜索
和test_*.py
,并导入(测试文件中的代码会自动执行)*_test.py
- 在导入的模块手机以下特征的对象,作为测试用例
-
开头的函数test
-
开头类及其Test
开头方法 (这个类不应该有test
)__init__
- unittest框架的测试用例
-
惯例(约定):和测试相关的一切,用
test
或者
test_
开头
1.4 读懂测试结果
import pytest
def test_ok():
print("ok")
def test_fail():
a, b = 1, 2
assert a == b
def test_error(something):
pass
@pytest.mark.xfail(reason="always xfail")
def test_xpass():
pass
@pytest.mark.xfail(reason="always xfail")
def test_xfail():
assert False
@pytest.mark.skip(reason="skip is case")
def test_skip():
pass
pytest报告分为几个基本部分:
- 报告头
- 用例收集情况
- 执行状态
- 用例的结果
- 进度
- 信息
- 错误信息
- 统计信息
- 耗时信息
报告中的结果缩写符合是什么含义
符号 | 含义 |
---|---|
. | 测试通过 |
F | 测试失败 |
E | 出错 |
X | XPass 预期外的通过 |
x | xfailed 预期失败 |
s | 跳过用例 |
如果要展示更加详细的结果,可以通过参数的方式设置
pytest -vrA
2. 断言
2.1 普通断言
pytest使用python内置关键字
assert
验证预期值和实际值
def test_b():
a = 1
b = 2
assert a == b
pytest 和python处理方式不一样:
- 数值比较:会显示具体数值
2.2 容器型数据断言
如果是两个容器型数据(字符串、元组、列表、字典、数组),断言失败,会将两个数据进行diff比较,找出不用
def test_b():
a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
b = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
assert a == b, "a和b不相等"
> assert a == b, "a和b不相等"
E AssertionError: a和b不相等
E assert [1, 1, 1, 1, 1, 1, ...] == [1, 1, 1, 1, 1, 1, ...]
E At index 9 diff: 0 != 2
E Full diff:
E - [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
E ? ^
E + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
E ?
2.3 断言出现异常
一般情况,:
- 执行测试用例出现了异常,认为失败
- 如果没有出现异常,认为通过。
“断言出现异常” :
- 出现了异常,认为通过
- 没有出现异常,认为失败
def test_b():
with pytest.raises(ZeroDivisionError):
1 / 0
不仅可以断言出现了异常,还可以断言出现什么异常,更可以断言谁引发的异常
def test_b():
d = dict()
with pytest.raises(KeyError) as exc_info:
print(d["a"]) # 这行代码,预期不发生异常
print(d["b"]) # 这行代码,预期异常
assert "b" in str(exc_info.value)
2.4 断言出现警告
警告(Warning)是Exception的子类,但是它不是有
raise
关键字抛出,而是通过
warnings.warn
函数进行执行。
def f():
pass
warnings.warn("再过几天,就要放假了", DeprecationWarning)
def test_b():
with pytest.warns(DeprecationWarning):
f()
3. 夹具
单元代码?
创建测试用例:
- 创建test_开头的函数
- 在函数内使用断言关键字
一个测试用例的执行分为四个步骤:
- 预置条件
- 执行动作
- 断言结果
- 清理现场
为了重复测试结果不会异常,也为了不会干扰其他用例。
在理想情况,为了突出用例重点,用例中应该只有2(执行动作)和3(断言结果)
- 1 和4 应当封装起来
- 1 和4 能够自动执行
夹具(Fixture)是软件测试装置,作用和目的:
- 在测试开始前,准备好相关的测试环境
- 在测试结束后,销毁相关的内容
以socket聊天服务器作为例子,演示夹具的用法
socket服务的测试步骤:
- 建立socket连接
- 利用socket执行测试动作
- 对结果进行断言
- 断开socket
3.1 创建夹具
3.1.1 快速上手
夹具的特性:
- 在测试用例之前执行
- 具体重复性和必要性
夹具client:自动和server建立socket连接,并供用例使用
创建一个函数,并使用
@pytest.fixture()
装饰器即可
@pytest.fixture()
def client():
client = socket.create_connection(server_address, 1)
return client
3.1.2 setup 和 teardwon
pytest 有2种方式实现teardwon,这里只推荐一种: 使用yield关键字
函数中有了
yield
关键字之后,成了生成器,可以多次调用
@pytest.fixture()
def server():
p = Process(target=run_server, args=(server_address,))
p.start() # 启动服务端
print("启动服务端")
yield p
p.kill()
yield
关键字 使夹具执行分为2个部分:
-
之前的代码,在测试前执行,对应xUnit中setUPyield
-
之后的代码,在测试后执行,对应xUnit中yeadDownyield
3.1.3 夹具范围
夹具生命周期:
- 被需要用的时候创建
- 在结束范围的时候销毁
- 如果夹具存在,不会重复创建
pytest夹具范围有5种:
- function:默认的范围,夹具在单个用例结束的时候被销毁
- class: 夹具在类的最后一个用例结束的时候被销毁
- module:夹具在模块的最后一个用例结束的时候被销毁
- package:夹具在包的最后一个用例结束的时候被销毁
- session:夹具在整个测试活动的最后一个用例结束的时候被销毁
使用Python,如果完全不会class,是没有任何问题的。
@pytest.fixture(scope="session")
def server():
p = Process(target=run_server, args=(server_address,))
p.start() # 启动服务端
print("启动服务端")
yield p
p.kill()
3.1.4 夹具参数化
夹具的参数,可以通过参数化的方式,为夹具产生多个结果 (产生了多个夹具)
如果测试用例要使用的夹具被参数化了,那么测试用例得到的夹具结果会有多个,每个夹具都会被使用
测试用例也会执行多次
测试用例,不知道自己被执行了多次,正如它不知道夹具被参数一样
@pytest.fixture(scope="session", params=[9001, 9002, 9003])
def server(request):
port = request.param
p = Process(target=run_server, args=(("127.0.0.1", port),))
p.start() # 启动服务端
print("启动服务端") # *3
yield p
p.kill()
3.2 使用夹具
3.2.1 在用例中使用
3.2.2 在夹具中使用
注意:夹具中使用夹具,必须确保范围是兼容的
例子:夹具A 和夹具B,A范围是
function
,B的范围是
session
,A可以使用B ,B不可用使用A
- A在第一个用例结束的时候,被销毁
- B在所有的用例结束的时候,被销毁
- A比B先被销毁
使用实际上依赖的关系:
假设:
- A使用B
- B的setup
- A
- B的tearDown
- B使用A (不可以的)
- 第一个用例结束的时候 A被销毁,B该怎么办?
- A的setUP
- B
- A的tearDown
生命周期短的夹具,才可用使用声明周期长的夹具
3.2.4 自动使用夹具
在一些代码质量工具中,未被使用的变量和参数,会被评为低质量。
pytest中,夹具可以声明自动执行,不需要写在用例参数列表中了。
@pytest.fixture(scope="function", autouse=True)
def server(request):
port = 9001
p = Process(target=run_server, args=(("127.0.0.1", port),))
p.start() # 启动服务端
print("启动服务端") # *3
yield p
p.kill()
4. 标记
默认情况下,pytest执行逻辑:
- 运行所有的测试用例
- 执行用例的时候,出现异常,判断为测试失败
- 执行用例的时候,没有出现异常,判断为测试通过
标记是给测试用例用的
标记的作用,就是为了改变默认行为:
- userfixtures :在测试用例中使用夹具
- skip:跳过测试用例
- xfail: 预期失败
- parametrize: 参数化测试,反复,多次执行测试用例
- 自定义标记:提供筛选用例的条件,pytest只执行部分用例
4.1 userfixtures
@pytest.mark.usefixtures("server",) # 只能给用例,使用夹具
class TestSocket:
def test_create_client(self, client):
print("客户端的地址", client.getsockname())
print("服务端的地址", client.getpeername())
def test_send_and_recv(self, client):
data = "hello world\n"
client.sendall(data.encode()) # 将字符串转为字节,然后发生
f = client.makefile()
msg = f.readline()
assert data == msg
def test_():
pass
4.2 skip 和 skipif
- skip 无条件跳过
- skipif 有条件跳过
class TestSocket:
@pytest.mark.skip(reason="心情不美丽,不想执行这个测试")
def test_create_client(self, client):
print("客户端的地址", client.getsockname())
print("服务端的地址", client.getpeername())
def test_send_and_recv(self, client):
data = "hello world\n"
client.sendall(data.encode()) # 将字符串转为字节,然后发生
f = client.makefile()
msg = f.readline()
assert data == msg
class TestSocket:
@pytest.mark.skipif(sys.platform.startswith("win"), reason="心情不美丽,不想执行这个测试")
def test_create_client(self, client):
print("客户端的地址", client.getsockname())
print("服务端的地址", client.getpeername())
4.3 xfail
无参数:无条件预期失败
有参数condition:有条件预期失败
有参数run: 预期失败的时候,不执行测试用例
有参数strict:预期外通过时,认为测试失败
@pytest.mark.xfail(1 != 1, reason="意料中的失败", run=False, strict=True)
def test_server_not_run():
"""当服务端未启动的时候,客户端应该连接失败"""
my_socket = socket.create_connection(server_address, 1)
4.4 参数化
好处:
- 提供测试覆盖率 1,1 => 2, 1,0=>1, 9999999999,1=>100000000
- 反复测试,验证测试结果稳定性 1,1 => 2 1,1 => 2 1,1 => 2
本质:同一个测试代码可以执行多个测试用例
@pytest.mark.parametrize("n", [1, "x"])
def test_server_can_re_content(n):
"""测试服务器可以被多个客户端反复连接和断开"""
print(n)
my_socket = socket.create_connection(server_address)
4.5 自定义标记
提供筛选用例的条件,使pytest只执行部分用例
- 选择简单的标记
-
pytest -m 标记
-
- 选择复杂的标记
-
同时具有标记A 和标记B的用例pytest -m "标记A and 标记B"
-
具有标记A 或标记B 的用例pytest -m "标记A or 标记B"
-
不具有标记A 的B用例pytest -m "not 标记A "
-
@pytest.mark.mmm
@pytest.mark.yumi
def test_sanmu():
pass
@pytest.mark.mmm
@pytest.mark.danny
def test_yiran():
pass
注册自定义标记:pytest知道哪些自定义标记是正确的,就不会发出警告
# pytest.ini
[pytest]
markers =
mmm
yumi
danny
5. 配置
5.1 配置方法
- 命令行
- 灵活
- 如果有多个选项的话,不方便
- 配置文件
- 特别适合大量,或者不常修改的选项
-
pytest.ini
-
pyproject.toml
- pytest 6.0+ 支持
- 是PEP标准
- 是未来
- python代码动态配置
- 太灵活, 意味着容易出错
- 优先级是最高的
# conftest.py 会被pytest自动加载,适合写配置信息
def pytest_configure(config): # 钩子:pytest会自动发现并运行这个函数
config.addinivalue_line("markers", "mmm")
config.addinivalue_line("markers", "yumi")
config.addinivalue_line("markers", "danny")
5.2 配置项
- 查询帮助信息
pytest -h
- 查看pytest参考文档 https://docs.pytest.org/en/stable/reference.html#id90
约定大于配置
6. 插件
一般情况,插件是一个python的包,在pypi,使用
pytest-
开头
不一般的情况,需要把插件的在
confgtest.py
进行启用
6.1 安装插件
pip install pytest-html
pip install pytest-httpx # mock httpx
pip install pytest-django # test django
6.2 使用插件
各个插件的使用方法 ,各不相同
参考各插件自己的问题
6.3 禁用插件
pytest -p no:插件名称
- 包名称:pytest-html
- 插件名称 :html
7. 布局
- 如果一个测试文件,存放在目录中,那么执行时,这个目录成为顶级目录
- 如果一个测试文件,存放在包中,那么执行时,根目录成为顶级目录
-
,将当前目录加入到python -m pytest
,当前目录中的模块可以被导入sys.path