跳转到主要内容

pytest插件,将预期输出卸载到数据文件

项目描述

pytest-golden

pytest插件,将预期输出卸载到数据文件

PyPI GitHub GitHub Workflow Status

简要使用说明

(另请参阅: example/)

安装pytest插件:

pip install pytest-golden

创建测试文件(例如 tests/test_foo.py

@pytest.mark.golden_test("test_bar/*.yml")
def test_bar(golden):
    assert foo.bar(golden["input"]) == golden.out["output"]

通配符选择“黄金”文件,这些文件既是测试的输入也是预期输出。测试基本上是基于文件参数化的。

创建一个或多个这样的YAML文件(例如 tests/test_bar/basic.yml

input: Abc
output: Nop

运行pytest来执行测试。

当测试下的函数发生变化时,其结果也可能发生变化,测试将无法通过。您可以运行pytest --update-goldens来自动重新填充输出。

请参阅详细使用说明

黄金测试的案例

考虑测试一个函数时的正常情况(例如,一个列出句子中所有单词的函数)。

foo.py

def find_words(text: str) -> list:
    return text.split()

tests/test_foo.py

from foo import find_words

def test_find_words():
    assert find_words("If at first you don't succeed, try, try again.") == [
        "If", "at", "first", "you", "don't", "succeed,", "try,", "try", "again."
    ]

您为该函数编写了基本的测试,但手动编写期望的输出可能相当繁琐,尤其是当输出内容较多时。有时,您可能首先编写一个模拟测试,然后从失败消息中复制实际输出。这并没有什么问题,因为这样您仍然会手动检查新的输出是否正确。

使用黄金测试

但让我们使用“黄金测试”重写这个测试。

tests/test_foo.py

from foo import find_words

def test_find_words(golden):
    golden = golden.open("test_find_words/test_basic.yml")
    assert find_words(golden["input"]) == golden.out["output"]

这里 golden["xxx"] 将直接从相关文件读取一个值。让我们创建这个(YAML)文件

tests/test_find_words/test_basic.yml

input: |-
  If at first you don't succeed, try, try again.

与输入不同,golden.out["yyy"] 的工作方式略有不同。通常它也将是测试的输入,从文件(断言将是一个完全正常的 pytest 断言)中获取,但在特殊“更新”模式下,它将接受运行时生成的任何结果并将其放回“黄金”文件。更新文件和最初填充文件都是通过命令 pytest --update-goldens 自动完成的。

tests/test_find_words/test_basic.yml

input: |-
  If at first you don't succeed, try, try again.
output:
- If
- at
- first
- you
- don't
- succeed,
- try,
- try
- again.

现在,当仅运行 pytest 时,测试将始终断言结果与预期输出完全相同。这正是单元测试的工作方式。

现在,您可以将其添加到源控制系统中。

引入更改

假设您对标点符号与单词混合在一起的情况不满意,因此您为该函数设计了不同的实现。

foo.py

import re

def find_words(text: str) -> list:
    return re.findall(r"\w+", text)

您还希望为其添加另一个测试用例

tests/test_find_words/test_quotation.yml

input: |-
  Dr. King said, 'I have a dream.'
output:
- Dr
- King
- said
- I
- have
- a
- dream

让我们将其转换为 参数化 的黄金测试(为每个匹配通配符的文件生成一个测试用例)

tests/test_foo.py

import pytest
from foo import find_words

@pytest.mark.golden_test("test_find_words/*.yml")
def test_find_words(golden):
    assert find_words(golden["input"]) == golden.out["output"]

现在,如果我们运行 pytest -v,我们会看到新的测试一切正常,它被识别为 test_find_words[test_quotation.yml],但是代码更改也导致之前的测试现在不一致!您将收到一个正常的 pytest 失败消息。

在这种情况下,您通常会回到测试文件并编辑期望的输出(如果您确实期望它发生变化)。但使用这种方式,您只需运行 pytest --update-goldens,您会看到“黄金”文件会自动更新(没有测试失败)。生成的差异可以在源控制系统中查看。

--- a/tests/test_find_words/test_basic.yml
+++ b/tests/test_find_words/test_basic.yml
@@ -5,8 +5,9 @@ output:
 - at
 - first
 - you
-- don't
-- succeed,
-- try,
+- don
+- t
+- succeed
 - try
-- again.
+- try
+- again

现在,您可以(以及潜在的代码审查人员)决定这个差异是否可以接受,或者是否需要更多更改。您可以再次迭代代码,单元测试也会随着您的迭代而更新,您永远不需要手动编辑它——只需视觉检查更改并进行提交。

用法

golden 修复

golden 参数添加到您的 pytest 测试函数中,它将传递一个 GoldenTestFixtureFactory

GoldenTestFixtureFactory

golden.open(path) -> GoldenTestFixture

golden 对象上调用此方法以获取一个实际可用的 修复

参数 path 是一个路径,相对于调用 Python 测试文件。在测试函数结束时自动进行清理。

@pytest.mark.golden_test(*patterns: str)

使用此装饰器来

  1. 避免调用 .open 并直接将 适当的修复 作为测试函数的 golden 参数,并
  2. 将参数化添加到“黄金”测试中。

patterns 是一个或多个相对于调用 Python 测试文件的 glob 模式。将为每个匹配的文件创建一个测试。

GoldenTestFixture

golden[input_key: str] -> Any

从相关的 YAML 文件中获取一个值,在顶层键中。可能会引发 KeyError

golden.get(input_key: str) -> Optional[Any]

与此类似,但如果键不存在,则返回 None

golden.out[output_key: str] -> Any

  • 在正常模式下

    从相关的 YAML 文件中获取一个值,在顶层键中。可能会引发 KeyError

  • 如果传递了 --update-goldens 标志

    获取用于键的代理对象,当进行比较(并随后断言)时,标记“黄金”文件应该为此顶级键获取更新值。这些更新在测试套件拆除时执行:原始文件总是重写一次。

golden.out.get(output_key: str) -> Optional[Any]

与此类似,但当与 None 进行比较时,将键标记为已从文件中删除,而不仅仅是具有 None 的值。

如何...

创建一个在 YAML 中可表示的自定义类型

我们将让这些类型被底层实现所知 - ruamel.yaml,但让我们只使用模块 pytest_golden.yaml 提供的透传函数。最好在 conftest.py 中全局应用。

import pytest_golden.yaml

pytest_golden.yaml.register_class(MyClass)

(并参见 ruamel.yaml 的详细信息)

如果您的类等同于单个值,则的备用示例

class MyClass:
    def __init__(self, value: str):
        self.value = value

pytest_golden.yaml.add_representer(MyClass, lambda dumper, data: dumper.represent_scalar("!MyClass", data.value))
pytest_golden.yaml.add_constructor('!MyClass', lambda loader, node: MyClass(node.value))

或者在继承标准类型的特定情况下,您完全可以完全省略标签,并依赖于与基类型的相等性。

class MyClass(str):
    pass

pytest_golden.yaml.add_representer(MyClass, lambda dumper, data: dumper.represent_str(data))

为模块中的所有测试应用默认的黄金文件

考虑这个只使用 pytest_golden 存储输出的测试

注意:即使这些 *.yml 文件是空的,也需要手动创建它们。

def test_foo(golden):
    golden = golden.open("stuff/test_foo.yml")
    assert foo() == golden.out["output"]

def test_bar(golden):
    golden = golden.open("stuff/test_bar.yml")
    assert bar("a", "b") == golden.out["output"]

测试体是不同的(因此通过 mark 应用模式是不适用的),但我们仍然希望自动分配黄金文件,而无需重复。

为此,我们可以像这样增强 golden 仪器

@pytest.fixture
def my_golden(request, golden):
    return golden.open(f"stuff/{request.node.name}.yml")

def test_foo(my_golden):
    assert foo() == my_golden.out["output"]

def test_bar(my_golden):
    assert bar("a", "b") == my_golden.out["output"]

在这里,YAML 文件的名称基于测试名称。以前文件名是手动确保匹配的。所以这两个片段是完全等价的。

请注意,您甚至不需要想出一个像 my_golden 这样的单独名称,只需覆盖整个模块的原始 golden 仪器即可。

请参阅 此示例的真实示例

项目详情


下载文件

下载适合您平台的文件。如果您不确定要选择哪一个,请了解更多关于 安装包 的信息。

源分布

pytest-golden-0.2.2.tar.gz (13.9 kB 查看哈希值)

上传时间

构建分布

pytest_golden-0.2.2-py3-none-any.whl (10.3 kB 查看哈希值)

上传时间 Python 3

由以下组织支持