Python monkeypatching 最佳实践

Python monkeypatching best practices

我正在测试一个具有多个外部依赖项的应用程序,并且我使用了 monkeypatching 技术通过自定义实现来修补外部库的功能,以帮助我的测试。它按预期工作。

但我目前遇到的问题是,这让我的测试文件变得非常混乱。我有几个测试,每个测试都需要自己实现修补函数。

例如,假设我有一个来自外部库的 GET 函数,我的 test_a() 需要修补 GET() 以便 returns False 和 test_b() 需要 GET() 进行修补,以便 returns 正确。

处理这种情况的首选方法是什么。目前我在做以下事情:

def test_a(monkeypatch):
    my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)

def test_b(monkeypatch)
    my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)

def test_c(monkeypatch)
    my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = True)

def my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = False):

    def patch_func_pos():
        return True

    patch_func_neg():
        return False

    patch_func_exception():
        raise my_exception

    if patch_get_to_return_true:
        monkeypatch.setattr(ExternalLib, 'GET', patch_func_pos)

    if patch_get_to_return_false:
        monkeypatch.setattr(ExternalLib, 'GET', patch_func_neg)

    if patch_get_to_raise_exception:
        monkeypatch.setattr(ExternalLib, 'GET', patch_func_exception)

上面的示例只有三个测试来修补一个函数。我的实际测试文件有大约20个测试,每个测试都会进一步修补几个功能。

有人可以建议我更好的处理方法吗?是否建议将 monkeypatching 部分移动到单独的文件中?

在不知道更多细节的情况下,我建议将 my_patcher 分成几个小装置:

@pytest.fixture
def mocked_GET_pos(monkeypatch):
    monkeypatch.setattr(ExternalLib, 'GET', lambda: True)


@pytest.fixture
def mocked_GET_neg(monkeypatch):
    monkeypatch.setattr(ExternalLib, 'GET', lambda: False)


@pytest.fixture
def mocked_GET_raises(monkeypatch):
    def raise_():
        raise Exception()
    monkeypatch.setattr(ExternalLib, 'GET', raise_)

现在使用 pytest.mark.usefixtures 在测试中自动应用夹具:

@pytest.mark.usefixtures('mocked_GET_pos')
def test_GET_pos():
    assert ExternalLib.GET()


@pytest.mark.usefixtures('mocked_GET_neg')
def test_GET_neg():
    assert not ExternalLib.GET()


@pytest.mark.usefixtures('mocked_GET_raises')
def test_GET_raises():
    with pytest.raises(Exception):
        ExternalLib.GET()

不过,还有改进的余地,要看实际情况。例如,当测试逻辑相同并且唯一不同的是某些测试先决条件(例如在您的情况下 GET 的不同补丁)时,测试或固定装置参数化通常会节省大量代码重复。假设您有一个内部调用 GET 的函数:

# my_lib.py

def inform():
    try:
        result = ExternalLib.GET()
    except Exception:
        return 'error'
    if result:
        return 'success'
    else:
        return 'failure'

并且您想测试它 returns 是否是一个有效的结果,无论 GET 的行为如何:

# test_my_lib.py

def test_inform():
    assert inform() in ['success', 'failure', 'error']

使用上述方法,您需要复制 test_inform 三次,副本之间的唯一区别是使用了不同的夹具。这可以通过编写一个参数化的夹具来避免,该夹具将接受 GET:

的多个补丁可能性
@pytest.fixture(params=[lambda: True,
                        lambda: False,
                        raise_],
                ids=['pos', 'neg', 'exception'])
def mocked_GET(request):
    monkeypatch.setattr(ExternalLib, 'GET', request.param)

现在将 mocked_GET 应用于 test_inform 时:

@pytest.mark.usefixtures('mocked_GET')
def test_inform():
    assert inform() in ['success', 'failure', 'error']

您可以从一个测试中获得三个测试:test_inform 将 运行 三次,每个模拟一次传递给 mocked_GET 参数。

test_inform[pos]
test_inform[neg]
test_inform[exception]

测试也可以参数化(通过 pytest.mark.parametrize),如果正确应用,参数化技术可以节省大量样板代码。