跳转到主要内容

一个小型的pytest实用工具,用于轻松创建测试替身

项目描述

pytest-call-checker

Deployed to PyPI GitHub Repository Continuous Integration Coverage MIT License Contributor Covenant

pytest-call-checker 是一个pytest插件,提供 checker 代码片段,允许创建具有有趣属性的测试替身。

描述问题

想象您正在编写一个库,该库会向API进行HTTP调用。如果您遵循将I/O与逻辑分离的常规做法,那么您可能会使用某种形式的依赖注入:您将编写的函数将接收一个HTTP客户端对象,并将HTTP调用路由到该对象。目的是在真实环境中,它们将接收一个真实的HTTP客户端,而在测试代码中,它们将接收一个不会实际执行HTTP调用的假客户端。

这个假客户端是测试替身。您通常希望检查以下内容:

  • 函数确实按预期进行了相同数量的调用;
  • 每次调用的参数都是正确的。

此外,您还希望能够根据输入和/或调用的顺序来控制每次调用的输出。

通常的做法是使用模拟或专门的库,如 requests-mockresponses 等。

这个库提供了一种其他的方法,它也可以在其他环境中工作:例如,可能你调用子进程,但你希望在单元测试中不调用它们。

想法

该库提供了一个名为checker的低级固定装置,你可以将其用作自己专用固定装置的基础。

在定义你的checker固定装置时,你会告诉它你期望的调用看起来像什么,以及如何创建适当的响应。

在你的测试中,你将使用checker.register注册一个或多个期望的调用,每个调用都有相应的响应。

当你调用待测试的函数时,你会传递一个checker实例。每当实例被调用时,我们会检查调用参数是否与已注册的期望调用之一匹配,并使用相应的响应回答。

在测试结束时,如果没有接收到所有期望的调用,我们将确保测试失败。

下面有一些具体的例子。

代码

安装

$ pip install pytest-call-checker

简单用法:Http客户端

在这个例子中,我们创建了一个httpx.Client的测试替身。在测试中,我们注册了一个调用客户端的get方法。然后我们运行我们的函数。我们可以确信

  • 我们的函数调用了get方法
  • 带有正确的参数
  • 并且客户端没有其他操作
  • 并且在调用方法时,它接收到了我们想要的假响应。
import httpx
import pytest

def get_example_text(client: httpx.Client):
    return client.get("https://example.com").text

@pytest.fixture
def http_client(checker):
    class FakeHttpClient(checker.Checker):

    return checker(checker.Checker(
        call=httpx.Client.request,
        response=httpx.Response
    ))

def test_get_example_text(http_client):
    http_client.register.get("https://example.com")(text="foo")

    assert get_example_text(client=http_client) == "foo"

更高级的用法:子进程

在这个例子中,我们创建了一个可调用的测试替身(subprocess.run),而不是对象的测试替身。这种用法稍微复杂一些,因为为了实例化我们的响应对象subprocess.CompletedProcess,我们需要知道传递给subprocess.run调用的命令args。如果我们需要在调用和响应中都重复args,这可能会有些烦人,所以我们在这里介绍一种技术,可以使我们的测试尽可能简单。

def get_date(run: Callable):
    return run(["date"]).stdout


@pytest.fixture
def subprocess_run(checker):
    class SubprocessRun(checker.Checker):
        call = subprocess.run

        def response(self, returncode, stdout=None, stderr=None):
            # When the response is implemented as a method of a `checker.Checker`
            # subclass, you can access `self.match`. This lets you construct
            # the output object using elements from the input kwargs, see
            # the `args` argument below.
            return subprocess.CompletedProcess(
                args=self.match.match_kwargs["popenargs"],
                returncode=returncode,
                stdout=stdout,
                stderr=stderr,
            )

    return checker(SubprocessRun())


def test_get_date(subprocess_run):
    subprocess_run.register(["date"])(returncode=0, stdout="2022-01-01")

    assert get_date(run=subprocess_run) == "2022-01-01"

如你所见,上面的代码中有两种创建你的checker.Checker实例的方法

  • 你可以直接创建一个实例
  • 或者子类化checker.Checker。在这种情况下,你可以在构造函数中传递callresponse,也可以将它们定义为方法。

如果你选择子类化,这让你可以在需要时通过self访问额外的元素。这是一个高级用法,我们将尽力避免破坏它,但它涉及到我们对象的内部工作原理,所以如果你真的做了复杂的事情,它可能会被破坏。

def response(self, ...)中可以访问的、最有用的checker.Checker属性应该是

  • self.match:与我们要构建的响应相关联的Match对象。
  • self.request_kwargs:测试替身被调用时使用的关键字参数

其他有趣的功能

使用函数进行匹配

有时,你无法对输入参数进行精确匹配,但你仍然想要检查一些属性以执行匹配。

在这种情况下,使用可调用对象而不是值作为要检查的参数的值。

import uuid

def create_object(client: ApiClient) -> ApiResponse:
    # Create object with a random ID
    return client.create(id=uuid.uuid4())


@pytest.fixture
def api_client(checker):
    class FakeAPIClient(checker.Checker):

    return checker(checker.Checker(
        call=ApiClient,
        response=ApiResponse
    ))


def test_get_date(api_client):
    def is_uuid(value):
        return isinstance(value, uuid.UUID)

    api_client.register(id=is_uuid)()

    assert create_object(client=api_client) == ApiResponse()

允许以不同顺序调用

默认情况下,期望的调用将以与注册相同的顺序执行。你实际上可以通过在创建固定装置时传递ordered=False来改变这一点。

import uuid

def fetch_objects(client: ApiClient, ids: set[int]) -> set[ApiResponse]:
    # Because it's in a set, we can't be sure of the call order
    return {
        client.get(id=id)
        for id in ids
    }


@pytest.fixture
def api_client(checker):
    class FakeAPIClient(checker.Checker):

    return checker(checker.Checker(
        call=ApiClient,
        response=ApiResponse,
        ordered=False,
    ))


def test_get_date(api_client):

    api_client.register(id=1)(id=1)
    api_client.register(id=2)(id=2)

    responses = fetch_objects(client=api_client, ids={1, 2})
    assert responses == {ApiResponse(id=1), ApiResponse(id=2)}

注意事项

有些事情并不理想,可能需要改进

  • 没有方法可以标记一个调用为可选的。我们假设如果测试是可重复的,那么我们应该始终知道它们是否会执行调用。
  • 可能还无法为模块创建测试替身。通常通过函数或类实例来进行依赖注入。

项目详情


下载文件

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

源分发

pytest_call_checker-1.0.6.tar.gz (8.8 kB 查看哈希值)

上传时间

构建分发

pytest_call_checker-1.0.6-py3-none-any.whl (7.6 kB 查看哈希值)

上传时间 Python 3

支持者