跳转到主要内容

自动创建mock的pytest插件

项目描述

pytest-automock

Github actions status for master branch Codecov coverage for master branch Pypi version Pypi downloads count

原因

  • 没有通用的automock解决方案

功能

  • pytest插件
  • 自动生成/自动使用函数和对象的mock
  • 同步和异步支持
  • 锁定模式以确保mock对象保持未更改
  • 可定制的序列化

限制

  • 不支持dunder方法(未来可能部分解决)
  • 不支持同步/异步生成器/上下文
  • 由于顺序很重要,竞争条件可能导致测试失败
  • 非确定性的表示会破坏测试,因为表示是调用快照键的一部分

许可证

pytest-automock在MIT许可证下提供。

需求

  • python 3.6+

用法

假设你有一个模块mymod.py

import time

class Network:
    def get_data_from_network(self, x, y):
        time.sleep(1)
        return x + y

    def send_data_to_network(self, value):
        time.sleep(1)

def logic(x):
    n = Network()
    a, b = 0, 1
    while b < x:
        c = n.get_data_from_network(a, b)
        a, b = b, c
        n.send_data_to_network("ok")
    return b

并且你想要为你的Network类创建mock(由于测试时间和合理的计数),但你太懒惰了,不想写它们... conftest.py

import pytest
import mymod

@pytest.fixture(autouse=True)
def _mocks(automock):
    with automock((mymod, "Network")):
        yield

你也可以使用模块路径表示法

import pytest

@pytest.fixture(autouse=True)
def _mocks(automock):
    with automock("mymod.Network"):
        yield

test_logic.py:

from mymod import logic

def test_logic():
    assert logic(7) == 8
    assert logic(10) == 13

如果你在这个设置上运行pytest,那么你会看到失败

$ pytest -x
...
E           RuntimeError: Mock is locked, but '__init__' wanted

automock可以在两种模式下工作:锁定和解锁。锁定模式是默认模式,不允许在此模式下调用mock对象的实际方法。所以,上面的错误表示我们不能调用我们的Network__init__。在锁定模式下也没有mock文件的更新。

为了允许实际调用和mock生成,automockpytest提供了额外的命令行参数:--automock-unlocked

$ pytest -x --automock-unlocked
...
test_logic.py .
...
1 passed in 22.09s

之后,你可以看到tests/mocks/test_logic/mymod/Network文件已被创建。这是你的测试序列的mock。现在你可以重新运行测试并查看发生了什么(你可以省略--automock-unlocked键以确保真实对象不会被触摸(实际上甚至创建))。

$ pytest -x
...
test_logic.py .
...
1 passed in 0.04s

API

automock (.fixture)

automock fixture是一个上下文管理器

def automock(*targets,
             storage: Union[str, Path] = "tests/mocks",
             override_name: Optional[str] = None,
             unlocked: Optional[bool] = None,
             remove: Optional[bool] = None,
             encode: Callable[[Any], bytes] = default_encode,
             decode: Callable[[bytes], Any] = default_decode,
             debug: Optional[Callable[[Dict, Call, Optional[Call]], None]] = None)
  • *targets:对象/模块和属性名(str)或带有点分隔符的对象/函数的模块路径((mymod, "Network")"mymod.Network")的配对/元组
  • storage:存储mock的根路径
  • override_name:强制mock文件名
  • unlocked:模式选择器(如果省略,则由--automock-unlocked选择)
  • remove:在测试运行前移除测试模拟(如果省略,则由--automock-remove选择)
  • encode:编码程序
  • decode:解码程序
  • debug:调试失败用例的函数,当你不明白为什么automock无法进行时。参数包括
    • memory:当前失败的测试
    • call_wanted:你想要立即进行的调用
    • call_saved:你上一次为这个测试生成模拟时保存的调用

call_wantedcall_saved是丰富的Call类对象,可以在mock.py文件中检查。你也可以使用一个"pdb"字符串代替你自己的函数作为debug参数值,以使用带有pdb.set_trace()指令的内部函数。默认的encode/decode程序使用picklegzip

automock_unlocked(固定装置)

从cli参数获取默认模式的固定装置(布尔值)。

automock_remove(固定装置)

从cli参数获取默认模式的固定装置(布尔值)。

automock(函数)

automock函数不应由除automock固定装置以外的任何人使用

def automock(factory: Callable, *,
             memory: Dict,
             locked: bool = True,
             encode: Callable[[Any], bytes] = default_encode,
             decode: Callable[[bytes], Any] = default_decode,
             debug: Optional[Callable[[Dict, Call, Optional[Call]], None]] = None):
  • factory:要包装的对象/函数
  • memory:获取/放置模拟的字典
  • locked:模式选择器
  • encode:编码程序
  • decode:解码程序
  • debug:与固定装置相同

默认的encode/decode程序使用picklegzip

注意事项

顺序

正如功能段落所描述的:“顺序计数”。这是什么意思?

函数

模拟函数/协程调用顺序计数。如果你模拟序列

func(1, 2)
func(2, 3)

并尝试使用模拟数据与序列

func(2, 3)
func(1, 2)

你会得到一个错误,因为调用顺序是确定测试想法的一部分

对象

模拟对象具有相同的行为,但方法调用是单独的,所以如果你模拟序列

t1 = T(1)
t2 = T(2)
t1.func(1, 2)
t2.func(2, 3)

则方法调用顺序是单独的,所以这是可以的

t1 = T(1)
t2 = T(2)
t2.func(2, 3)
t1.func(1, 2)

但不是对于__init__方法,因为模拟是内部附加到实例的

t2 = T(2)
t1 = T(1)
t1.func(1, 2)
t2.func(2, 3)

将会失败

函数参数

内部,模拟的关键是由实例编号和调用编号组成的。这导致一些“不明显”的行为

import time
from pytest_automock import automock

def nop(x):
    return x

m = {}
mocked = automock(nop, memory=m, locked=False)
mocked(time.time())

mocked = automock(nop, memory=m, locked=True)
mocked(time.time())

将会因为模拟创建时的参数与模拟使用时的参数不同而失败。同样,如果pickle表示不是确定的,这也会破坏模拟。

开发

运行测试

由于覆盖率问题/功能,插件覆盖率默认已损坏。解决方案

COV_CORE_SOURCE=pytest_automock COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage.eager pytest

由以下组织支持