Python Monkeypatching最佳做法

时间:2018-07-18 13:46:30

标签: python mocking pytest monkeypatching

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

但是我目前遇到的问题是,这使我的测试文件非常混乱。我有几个测试,每个测试都需要自己执行补丁功能。

例如,假设我有一个来自外部库的GET函数,我的test_a()需要修补GET(),以便它返回False,而test_b()需要{{1} }进行修补,使其返回True。

处理这种情况的首选方法是什么。目前,我正在执行以下操作:

GET()

上面的示例只有三个测试可以修补一个功能。我的实际测试文件有大约20个测试,每个测试将进一步修补几个功能。

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

1 个答案:

答案 0 :(得分:1)

在不知道更多细节的情况下,我建议将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'

,并且您想测试无论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),正确应用参数化技术可以节省大量样板代码。