一个小型的pytest实用工具,用于轻松创建测试替身
项目描述
pytest-call-checker
pytest-call-checker
是一个pytest插件,提供 checker
代码片段,允许创建具有有趣属性的测试替身。
描述问题
想象您正在编写一个库,该库会向API进行HTTP调用。如果您遵循将I/O与逻辑分离的常规做法,那么您可能会使用某种形式的依赖注入:您将编写的函数将接收一个HTTP客户端对象,并将HTTP调用路由到该对象。目的是在真实环境中,它们将接收一个真实的HTTP客户端,而在测试代码中,它们将接收一个不会实际执行HTTP调用的假客户端。
这个假客户端是测试替身。您通常希望检查以下内容:
- 函数确实按预期进行了相同数量的调用;
- 每次调用的参数都是正确的。
此外,您还希望能够根据输入和/或调用的顺序来控制每次调用的输出。
通常的做法是使用模拟或专门的库,如 requests-mock,responses 等。
这个库提供了一种其他的方法,它也可以在其他环境中工作:例如,可能你调用子进程,但你希望在单元测试中不调用它们。
想法
该库提供了一个名为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
。在这种情况下,你可以在构造函数中传递call
和response
,也可以将它们定义为方法。
如果你选择子类化,这让你可以在需要时通过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 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | db5ab8b4ca421f34249b25b23be477fe91bbd4c8f7baf303286f031ea35d5245 |
|
MD5 | 470235de7c21ccb265bb498eb33b3c34 |
|
BLAKE2b-256 | ecd219534dca043c469339f4a1f6711b512847351791ed684c8a637a01a07a3c |
pytest_call_checker-1.0.6-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 8f97575726174202f86bab3c6020945fcce9e90dfd4cd1862e9ca11b2bd293ab |
|
MD5 | 096b6b6218d1a2cc1c200c1d86555755 |
|
BLAKE2b-256 | 731706eb427b955e37154db8c45a2b8eecfb6c483454c468eb21b2727e523496 |