(全文约3200字,阅读约需7分钟,建议先收藏后阅读。首发于公众号:测试开发研习社,欢迎关注)
pytest 版本遵循 ( <major>.<minor>.<patch>
) 语义控制。
昨天发布的 pytest 8.0 是全新的 major
版本,
意味本次更新有重大变更,以及向后不兼容的修改!
1. 重大变更
01. 移除历史包袱
一大批弃用警告升级为错误,从而无法继续使用,并将在 8.1
正式从代码中移除。
如果你还没有了解新版变化,手滑升级到了 8.0
后无法正常运行,可以通过屏蔽警告的方式,暂时过渡
[pytest]
filterwarnings =
ignore::pytest.PytestRemovedIn8Warning
注意,这是一个临时解决方案,8.1 之后这个方法也会失效
02. Python >=3.8
Python 3.7 生命周期在 2023 年 6 月 27 日已终止。
pytest 自本次更新之后,放弃了对 python 3.7 的兼容。
03.pluggy >=1.3.0
pluggy 1.3 引入了新的基于生成器的新式钩子包装器(hookwrapper)
在旧式的钩子包装器中,获取和修改钩子结果,使用了面向对象式的方式:
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
outcome = yield # <--------- 生成器方式接收对象
res = outcome.get_result() # <--- 【面向对象】方式获取旧结果
new_res = post_process_result(res)
outcome.force_result(new_res) # <--- 【面向对象】方式设置新结果
而在新式的钩子包装其中,获取和修改钩子结果,直接使用生成器自身的语法
@pytest.hookimpl(wrapper=True) # 使用新参数申明 新式包装器
def pytest_pyfunc_call(pyfuncitem):
res = yield # <-------------- 生成器方式获取旧结果
new_res = post_process_result(res)
return new_res # <-------------- 生成器方式设置新结果
可以看到,新式写法简洁了很多,有更强烈的Python风格
04.区分包和目录
新增类:pytest.Directory
表示目录
既有类:pytest.Package
表示包
包和目录的概念得以加强,各司其职
假设有以下目录关系
myroot/
pytest.ini
top/
├── aaa
│ └── test_aaa.py
├── test_a.py
├── test_b
│ ├── __init__.py
│ └── test_b.py
├── test_c.py
└── zzz
├── __init__.py
└── test_zzz.py
在此前的 pytest 版本中,被处理为以下结果
<Session>
<Module top/test_a.py>
<Function test_it>
<Module top/test_c.py>
<Function test_it>
<Module top/aaa/test_aaa.py>
<Function test_it>
<Package test_b>
<Module test_b.py>
<Function test_it>
<Package zzz>
<Module test_zzz.py>
<Function test_it>
本次更新之后,会被处理为这样
<Session>
<Dir myroot>
<Dir top>
<Dir aaa>
<Module test_aaa.py>
<Function test_it>
<Module test_a.py>
<Function test_it>
<Package test_b>
<Module test_b.py>
<Function test_it>
<Module test_c.py>
<Function test_it>
<Package zzz>
<Module test_zzz.py>
<Function test_it>
05. 调整用例收集顺序
此前,用例收集顺序是先文件、后目录
此次更新中,用例收集按照字母顺序进行
这一点请macOS用户特别关注,此前因操作系统的差异,macOS中用例执行顺序和Windows中有细微差异。
此次更新可能回到结果造成影响
具体例子可回顾上一小节中收集结果
06. 断言警告
pytest 提供了一个对警告进行断言的方式
def test_is_user_waring():
# 出现指定类型警告则测试通过,否则失败
with pytest.warns(UserWarning):
warnings.warn(f"这是一条用户警告", UserWarning)
warnings.warn(f"这是一条语法警告", SyntaxWarning)
在此前的版本中执行结果如下(无事发生...)
===================== test session starts ==================
platform win32 -- Python 3.12.0, pytest-7.4.0, pluggy-1.0.0
rootdir: D:\pytest_7.2.x
configfile: pytest.ini
collected 1 item
test_show_warnings.py . [100%]
===================== 1 passed in 0.01s =====================
自 8.0
开始,warns
只会捕获它所断言的警告类型,至于其他警告,则会重新抛出,执行结果如下
===================== test session starts ==================
platform win32 -- Python 3.12.0, pytest-8.0.0, pluggy-1.4.0
rootdir: D:\pytest_8.0.x
collected 1 item
test_show_warnings.py . [100%]
===================== warnings summary =====================
test_show_warnings.py::test_is_user_waring
D:\pytest_8.0.x\test_show_warnings.py:10: SyntaxWarning: 这是一条语法警告
warnings.warn(f"这是一条语法警告", SyntaxWarning)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============== 1 passed, 1 warning in 0.01s =================
07. 净化 ini 配置的默认值
过去,对于未设置的 ini 配置,会根据类型返回空列表或空字符串,
于是出现了一个 bug:如果设置默认值 None ,会像未设置默认值一样,返回空列表
自 8.0
开始,对默认值的返回进行了更加具体地处理规则:
-
如果设置了默认值,返回默认值(哪怕默认值是 None)
-
没有设置默认值但申明了类型,根据类型返回空列表、空字符串或布尔值 False
-
没有设置默认,也没有申明类型,返回空字符串
2. 优化改进
01. 改进差异显示(diff)
建议安装
pygments
,增加色彩突出
对于同一个测试用例
def test_diff_list():
a = list('123')
b = list('123345')
pytest 7 的执行结果如下
(双击可放大)
更新至 8.0
后,差异显示更加直观
(双击可放大)
不仅告诉我们错了,还是告诉我们哪里错了;不仅告诉我们哪里错了,还告诉我们为什么错了,具体是哪个字符不对...
还有比这更贴心的测试框架吗?
02. 单独控制断言详细程度
如果想要像上一小节中那样显示断言的详细信息,需要添加命令行参数 -vv
pytest --vv
需要注意的是:-vv
不仅让断言信息更加详细,
也让整个终端输出更加详细,比如会更详细地显示版本信息、用例名、用例执行结果等。
如果只希望断言详细,而不需要其他信息变冗长呢?
8.0
新增了一个 ini 配置项目 verbosity_assertions
,可单独对断言详细程度进行控制
[pytest]
verbosity_assertions = 2
03. 新式钩子包装器
更加纯粹的,符合生成器风格的钩子包装器,
详见前文 1.3
04. 优化日志配置
这个bug 我在前几天读源码的时候发现了,本来准备提交补丁刷个贡献值,
不过已经被人在 9 月份就抢了先。。。
就说说 BUG 的原因:
首先,在 pytest 中对于日志内容有多种处理方式:
-
caplog_handler
:记录到 fixture 中,供用例使用 -
report_handler
:记录到 report 中,供测试报告使用 -
log_file_handler
:记录到文件中,形成日志文件 -
log_cli_handler
:记录到终端中,在命令行输出
然后 pytest 配置文件中有 3 种对日志的配置选项
-
log_cli_*
:作用于终端 -
log_file
_*:作用于文件 -
log_*
:作用于全局
直观上来说,如果 log_cli_*
或 log_file_*
没有配置的话,
应该读取 log_*
中的内容
或者,如果 log_cli_*
或 log_file_*
完全相同的话,不必重复配置 2 次,
应该直接对 log_*
进行配置
没错,代码中也是怎么写的。。。
但是!但是! log_cli_*
或 log_file_*
居然有默!认!值!
就算你真的没有对它们配置,它们也会读取到默认值而不是 log_*
中的内容,
导致相同的内容必须配置3次才能正常工作
在此次更新中,修复了这个BUG,统一和简化了日志的配置
05. 其他
还有一些改进,以文档和类型申明为主,
如有兴趣,可以点击文章底部【查看原文】进行了解
3. BUG 修复
略
已经修复的 BUG 我不太关注
4. 弃用规则
01. 测试用例不该有返回值
用例执行结果必须是 None,也就是不应该有返回值
三三三木,公众号:测试开发研习社pytest的内置插件盘点7:python
在此前的pytest版本中,如果用例返回非 None 的结果,会引发弃用警告。
按照计划,弃用警告也会在 8.0
中引发错误、无法使用,并在 8.1
彻底清除。
但是此次 8.0
发布的版中,如果用例返回非 None 的结果,只会引发普通警告,不再引发弃用警告。
这也意味着,pytest 开发团队不再认为用例返回结果是一个错误
02. fixture 不应该添加标记
usefixture 是 pytest 的内置标记
三三三木,公众号:测试开发研习社pytest的内置插件盘点5:fixtures
我们知道,给测试用例在参数列表中加上 fixture 名,或者 usefixtures
标记中假设 fixture 名,会自动请求该 fixture
import pytest
@pytest.fixture()
def f():
1/0
@pytest.mark.usefixtures('f')
def test_abc():
pass
执行结果
(双击可放大)
我们还知道,fixture 也可以请求 fixture,
所以给 fixture 在参数列表中加上 fixture 名,会自动请求该 fixture
import pytest
@pytest.fixture()
def ff():
assert False
@pytest.fixture()
def f(f):
1 / 0
@pytest.mark.usefixtures('f')
def test_abc():
pass
执行结果
(双击可放大)
但是请注意:给 fixture 加上 usefixtures
标记中 fixture 名,不会请求 fixture
因为标记只对用例有效,对 fixture 是无效的
(双击可放大)
自 8.0
开始,再犯这样的错误,会引发弃用警告。
其实我不只见过这种给给 fixture 加标记的操作,还见过给用例加 fixture
的操作。。
(双击可放大)
归根到底,是没有把 case 和 fixture 分清楚,强烈建议这种把操作也列入弃用
结语
老实说,我在刚看到对于 Pytest 8.0 正式发布时既兴奋又焦虑。
我的《pytest 源码剖析》才完成一半,就眼睁睁看着它从 7.0 更新到 7.2,又从 7.2 更新到 7.4,
现在又更新了个大版本到 8.0,真担心写的速度跟不上是它变得速度。
不过,在仔细研究了变更内容后,发现问题不大,预计 8.1 的时候可以写第二版本,就缩短距离了。
如果你 pytest 感兴趣的话欢迎点赞关注,希望本书能为你更深入了解 pytest 提供一些帮助