pytest mocker 夹具模拟模块来自定义它的地方而不是使用它的地方

Rah*_*mar 1 python unit-testing pytest pytest-mock

我有一些实用程序功能 src/utils/helper.py

想象一下,我func_a在 utils/helper.py 中有一个函数,它在我的项目中的多个地方使用。

每次我使用它时,我都会像这样导入它

from src.utils.helper import func_a
Run Code Online (Sandbox Code Playgroud)

现在我想func_a在我的测试中模拟这个。

我想在 conftest.py 中创建一个夹具,这样我就不需要为每个测试文件一次又一次地编写模拟函数。

问题是,在我的模拟函数中,我不能这样写。

https://pypi.org/project/pytest-mock/

mocker.patch('src.utils.helper.func_a', return_value="some_value", autospec=True)
Run Code Online (Sandbox Code Playgroud)

我必须为每个测试文件这样写

mocker.patch('src.pipeline.node_1.func_a', return_value="some_value", autospec=True)
Run Code Online (Sandbox Code Playgroud)

根据文档https://docs.python.org/3/library/unittest.mock.html#where-to-patch

因为我正在导入,func_a就像from src.utils.helper import func_a我必须模拟它的使用位置而不是它的定义位置。

但是这种方法的问题是我无法在 conftest.py 的夹具中定义它

目录结构

??? src
?   ??? pipeline
?   ?   ??? __init__.py
?   ?   ??? node_1.py
?   ?   ??? node_2.py
?   ?   ??? node_3.py
?   ??? utils
?       ??? __init__.py
?       ??? helper.py
??? tests
    ??? __init__.py
    ??? conftest.py
    ??? pipeline
        ??? __init__.py
        ??? test_node_1.py
        ??? test_node_2.py
        ??? test_node_3.py
Run Code Online (Sandbox Code Playgroud)

MrB*_*men 5

好吧,正如您所写,如果您使用from xxx import. 您的第一个选择当然是在生产代码中使用完整模块导入:

node_1.py

import src.utils.helper

def node_1():
    src.utils.helper.func_a()
Run Code Online (Sandbox Code Playgroud)

我相信你知道这一点,但我还是想提一下。

如果您不想更改生产代码,则必须根据您编写的修补模块进行修补。这基本上意味着您必须动态构建补丁位置。如果您对测试函数和测试函数具有对称命名,您可以执行以下操作:

conftest.py

@pytest.fixture
def mock_func_a(mocker, request):
    node_name = request.node.name[5:]  # we assume that the test function name is "test_node_1" for testing "node_1"
    module_path = f'src.pipeline.{node_name}.func_a'
    mocked = mocker.patch(module_path,
                          return_value="some_value",
                          autospec=True)
    yield mocked
Run Code Online (Sandbox Code Playgroud)

如果无法从测试本身导出补丁路径,则必须向测试函数添加更多信息。这可能只有在您想做的不仅仅是夹具中的补丁时才有意义 - 否则您也可以直接添加patch装饰器。
您可以添加具有模块路径或模块路径的一部分作为参数的自定义标记:

test_node_1.py

@pytest.mark.node("node_1")
def test_node(mock_func_a):
    node_1()
    mock_func_a.assert_called_once()
Run Code Online (Sandbox Code Playgroud)

conftest.py

@pytest.fixture
def mock_func_a(mocker, request):
    mark = next((m for m in request.node.iter_markers()
                 if m.name == 'node'), None)  # find your custom mark
    if mark is not None:
        node_name = mark.args[0]
        module_path = f'src.pipeline.{node_name}.func_a'
        mocked = mocker.patch(module_path,
                              return_value="some_value",
                              autospec=True)
        yield mocked
Run Code Online (Sandbox Code Playgroud)

或者,如果您需要提供完整路径:

test_node_1.py

@pytest.mark.path("src.pipeline.node_1")
def test_node(mock_func_a):
    ...
Run Code Online (Sandbox Code Playgroud)

conftest.py

@pytest.fixture
def mock_func_a(mocker, request):
    mark = next((m for m in request.node.iter_markers()
                 if m.name == 'path'), None)  # find your custom mark
    if mark is not None:
        node_name = mark.args[0]
        module_path = f'{node_name}.func_a'
        ...
Run Code Online (Sandbox Code Playgroud)