pytest03 The writing and reporting of assertions in tests
Asserting with the assert statement
断言 assert 陈述
pytest 允许你使用标准的python assert 用于验证Python测试中的期望和值。例如,可以编写以下内容:
1 | # content of test_assert1.py |
断言函数会返回某个值。如果此断言失败,你将看到函数调用的返回值:
1 | $ pytest test_assert1.py |
pytest 支持显示最常见的子表达式的值,包括调用、属性、比较以及二进制和一元运算符。(见 用pytest演示python失败报告 )这允许你在不丢失自省信息的情况下使用不带样板代码的惯用python构造。
但是,如果使用如下断言指定消息:
1 | assert a % 2 == 0, "value was odd, should be even" |
这样根本不进行断言内省,消息将简单地显示在追溯中。
Assertions about expected exceptions
关于预期异常的断言
为了编写有关引发的异常的断言,可以使用 pytest.raises 作为这样的上下文管理器:
1 | import pytest |
如果你需要访问实际的异常信息,可以使用:
1 | def test_recursion_depth(): |
excinfo 是一个 ExceptionInfo 实例,它是引发的实际异常的包装。感兴趣的主要特征是 .type , .value 和 .traceback .
你可以通过给上下文管理器传递一个match关键字参数,用于测试正则表达式是否匹配异常的字符串表示形式(类似于 unittest 中的 TestCase.assertRaisesRegexp 方法):
1 | import pytest |
match 方法的regexp参数与 re.search 函数相同,因此在上面的示例中 match='123' 也会起作用的。
- 在这个例子中,
test_match使用pytest.raises来检查f()是否抛出了ValueError。 - 如果
f()抛出了ValueError,测试通过。 - 如果
f()没有抛出任何异常,测试失败。 - 如果
f()抛出了其他异常,测试失败。
有另一种形式的 pytest.raises 函数,其中传递的函数将用给定的 *args 和 **kwargs 并断言引发了给定的异常:
1 | pytest.raises(ExpectedException, func, *args, **kwargs) |
如果出现故障,如 no exception or wrong exception ,这份报告将为你提供有益的输出。
请注意,也可以将“引发”参数指定为 pytest.mark.xfail ,它检查测试是否以比引发任何异常更具体的方式失败。
pytest.mark.xfail是一个标记,用于标记一个测试函数,表示这个测试预计会失败。如果测试确实失败了,那么pytest会报告这个测试为“xfail”(预期失败),而不是失败。使用
pytest.raises对于测试自己代码中故意引发的异常的情况,可能会更好。而使用@pytest.mark.xfail使用check函数可能更适合记录未修复的错误(测试描述了“应该”发生的情况)或依赖项中的错误。
举例:
1 |
|
- 在这个例子中,
test_f被标记为xfail,并且预期f()会抛出IndexError异常。 - 如果
f()抛出了IndexError,测试会被标记为“xfail”。 - 如果
f()没有抛出任何异常,测试会被标记为“xpass”(意外通过)。 - 如果
f()抛出了其他异常,测试会被标记为“failed”。
假设你有一个函数 process_data,在处理某些数据时会抛出 ValueError,但你知道这是一个已知的问题,尚未修复:
1 | def process_data(data): |
1 | python -m pytest .\test_xfail.py |
在这个测试中,test_process_data 被标记为 xfail,并且预期 process_data([]) 会抛出 ValueError。如果 process_data([]) 抛出了 ValueError,测试会被标记为“xfail”。如果 process_data([]) 没有抛出任何异常,测试会被标记为“xpass”。
使用场景:
- **
pytest.raises**:适用于测试你自己代码中故意引发的异常的情况。例如,你有一个函数在某些条件下会抛出特定的异常,你希望确保这些异常被正确抛出。 - **
@pytest.mark.xfail**:更适合记录未修复的错误(测试描述了“应该”发生的情况)或依赖项中的错误。例如,你知道某个测试在当前环境下会失败,但你希望在未来修复这个问题后,测试能够通过。如果是process_data(1),这个用例就会执行成功。
Making use of context-sensitive comparisons
利用上下文相关的比较
当遇到比较时pytest 对提供上下文敏感信息具有丰富的支持。例如:
1 | # content of test_assert2.py |
运行此模块:
1 | $ pytest test_assert2.py |
对一些情况进行特殊比较:
- 比较长字符串:显示文本差异
- 比较长序列:第一个失败下标
- 比较字典:不同的条目
Defining your own explanation for failed assertions
为失败的断言定义自己的解释
可以通过执行 pytest_assertrepr_compare 钩子来增加自己细节解释。
pytest_assertrepr_compare(config, op, left, right)返回失败的断言表达式中比较解释。如果没有自定义解释,则返回“None”,否则返回字符串列表。字符串将由换行符联接,但任何字符串中的换行符将被转义。请注意,除了第一行之外,其他所有行都将略微缩进,目的是为了将第一行作为摘要。
例如,考虑在 conftest.py 中增加如下的钩子,来为Foo对象提供替代解释:
1 | # content of conftest.py |
现在,假设这个测试模块:
1 | # content of test_foocompare.py |
你可以运行测试模块并在conftest文件中定义自定义输出:
1 | $ pytest -q test_foocompare.py |
Assertion introspection details
断言自省详细信息
通过在运行断言语句之前重写它们,可以获得有关失败断言的报告详细信息。重写的断言语句将自省信息放入断言失败消息中。 pytest 只重写由其测试收集过程直接发现的测试模块,因此 支持模块中的断言(本身不是测试模块)将不会被重写 .
你可以手动使能断言,为一个导入的模块重写,这个模块在导入前通过register_assert_rewrite调用。(一个很好的地方是在您的根目录中 conftest.py )
示例
假设你有一个支持模块 utils.py,其中包含一些辅助函数,并且你在测试模块中使用了这些函数。
1 | # utils.py |
1 | # test_utils.py |
默认行为
默认情况下,pytest 不会重写 utils.py 中的断言,因此 test_check_positive 失败时,你只会看到一个简单的 AssertionError,而不会看到详细的自省信息,部分如下:
1 | test_utils.py:3 (test_check_positive) |
手动启用断言重写
为了在 utils.py 中启用断言的重写,你可以在 conftest.py 中调用 pytest.register_assert_rewrite。
1 | # conftest.py |
运行测试
现在,当你运行 pytest 时,utils.py 中的断言也会被重写,你将获得详细的自省信息:
1 | test_utils.py:3 (test_check_positive) |
总结
- 断言自省:
pytest通过重写断言语句来提供详细的错误信息。 - 默认行为:
pytest只重写测试模块中的断言。 - 手动启用:通过
pytest.register_assert_rewrite可以在支持模块中启用断言的重写。 - 最佳实践:在
conftest.py中调用pytest.register_assert_rewrite来注册需要重写的模块。
Assertion rewriting caches files on disk
断言重写将文件缓存在磁盘上
pytest 会将重写的模块写回磁盘进行缓存(可以提高后续测试运行的性能,因为不需要每次都重新编译和重写模块)。
可以通过设置 sys.dont_write_bytecode = True,来禁用 .pyc 文件的缓存。
例如,为了避免在经常移动文件的项目中留下过时的.pyc文件。在一些项目中,文件经常被移动或重命名。如果 pytest 将重写后的模块缓存到磁盘上,可能会导致旧的 .pyc 文件残留,从而引起问题,比如重命名重跑用例会有意想不到的问题。禁用缓存可以避免这种情况。
1 | import sys |
请注意,你仍然可以获得断言内省的好处,唯一的变化是 .pyc 文件不会缓存在磁盘上。
此外,如果重写无法写入新的 .pyc 文件,它将自动跳过缓存,即只读文件系统或压缩文件中的文件。
Disabling assert rewriting
禁用断言重写
pytest 通过使用导入钩子hook的方式写入新的 pyc 文件夹,在导入时重写测试模块。大多数时候这是透明的。但是,如果你自己操作导入机制,导入钩子可能会产生干扰。假设你有一个项目,其中包含自定义的导入机制。在这种情况下,pytest 的断言重写机制可能会干扰你的自定义导入。
如果是这种情况,你有两个选择:
- 通过添加字符串
PYTEST_DONT_REWRITE到它的docstring,禁用特定模块的重写 。 - 通过使用
--assert=plain,禁用所有模块的重写.- 添加断言重写作为一个可替换的自省机制
- 介绍
--assert选项。不赞成使用--no-assert和--nomagic. - 移除
--no-assert和--nomagic选项。移除--assert=reinterp选项。
假设你有一个自定义的导入器 custom_importer.py,它修改了 sys.meta_path:
1 | # custom_importer.py |
测试模块
假设你有一个测试模块 test_module.py,其中包含一个简单的测试:
1 | # test_module.py |
运行测试
当你运行 pytest 时,pytest 的导入钩子可能会与你的自定义导入器发生冲突:
1 | pytest test_module.py |
禁用断言重写
为了禁用 pytest 的断言重写机制,你可以在运行 pytest 时使用 --no-assert 选项:
1 | pytest --no-assert test_module.py |
解释
--no-assert选项:这个选项告诉pytest不要使用断言重写机制。这样可以避免pytest的导入钩子与你的自定义导入机制发生冲突。- 影响:禁用断言重写后,你将失去断言自省的详细信息。也就是说,断言失败时,你只会看到简单的
AssertionError,而不会看到详细的上下文信息。