如何恢复模拟

How to revert Mocking

当我弄清楚为什么我的测试文件 运行 很顺利,但如果整个包的测试 运行 与 pytest 在一起时,我就崩溃了。

我有一个 parent class 和一个 child class。 child 像这样调用 parent 的初始化:

class Child(Parent):
    __init__(...,**kwargs):
        ...
        super().__init__(**kwargs)

我想用一个小测试来定义这个行为。

from unittest.mock import Mock

def test_init_calls_parent_init():
    Parent.__init__ = Mock()
    
    Child()

    assert Parent.__init__.called

问题是Parent.__init__ 对于其他文件中的所有以下测试,一直是模拟

我的想法是将它放入函数作用域中只是一个临时更改。当然,由于这些测试失败了,它们隐含地定义了对 parent init 的需求,但我也想通过一个显式测试来确保。

我应该创建一些 pytest setup/teardown 或者什么是防止这种情况的公认方法?

您可以尝试将父级转换为 fixture, which defaults to having a "function" scope,理论上应该在每次测试后拆除 fixture。

记得yield your fixture并在之后添加任何其他必要的拆卸说明。

你的 Parent 不解构模拟的原因是因为父 classes/functions 不是 fixtures 被视为任何其他 parent/child 函数。

当您执行 Parent.__init__ = Mock() 时,您基本上重新定义了模块本身的 __init__,然后反映在后续测试中。

与其手动将 Parent.__init__ 的实现更改为 Mock,我的建议是只使用 unittest and pytest-mock.

中已有的修补功能
  • 我们也可以按照@MattSom 的建议使用monkeypatch(见评论部分)

src.py

class Parent:
    def __init__(self, **kwargs):
        print("Parent __init__ called")

class Child(Parent):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print("Child __init__ called")

没有更正:

from unittest.mock import Mock, patch

from src import Child, Parent


def test_init_calls_real_parent_init():
    Child()


def test_init_calls_updated_parent_init():
    Parent.__init__ = Mock()

    Child()

    assert Parent.__init__.called


def test_init_calls_real_parent_init_2():
    Child()

输出:

$ pytest -q test_src.py -rP
...                                                                                                                                                                                                 [100%]
================================================================================================= PASSES ==================================================================================================
____________________________________________________________________________________ test_init_calls_real_parent_init _____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Parent __init__ called
Child __init__ called
___________________________________________________________________________________ test_init_calls_updated_parent_init ___________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
___________________________________________________________________________________ test_init_calls_real_parent_init_2 ____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
3 passed in 0.01s

调查结果:

第一个测试称为真正的Parent.__init__。第二次测试称为模拟。然而,在第三次测试中,它也意外地调用了第二次测试中制作的模拟。

修正后:

from unittest.mock import Mock, patch

from src import Child, Parent


def test_init_calls_real_parent_init():
    Child()


# Personally I wouldn't advise to do this. It just works :)
def test_init_calls_updated_parent_init():
    # Setup
    orig_parent_init = Parent.__init__  # Store original init

    # Real test
    Parent.__init__ = Mock()

    Child()

    assert Parent.__init__.called

    # Teardown
    Parent.__init__ = orig_parent_init  # Bring back the original init


@patch("src.Parent.__init__")  # Uses unittest
def test_init_calls_mocked_parent_init(mock_parent_init):
    Child()

    assert mock_parent_init.called


def test_init_calls_mocked_parent_init_2(mocker):  # Uses pytest-mock
    mock_parent_init = mocker.patch("src.Parent.__init__")

    Child()

    assert mock_parent_init.called


def test_init_calls_real_parent_init_2():
    Child()

输出:

$ pytest -q test_src_2.py -rP
.....                                                                                                                                                                                               [100%]
================================================================================================= PASSES ==================================================================================================
____________________________________________________________________________________ test_init_calls_real_parent_init _____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Parent __init__ called
Child __init__ called
___________________________________________________________________________________ test_init_calls_updated_parent_init ___________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
___________________________________________________________________________________ test_init_calls_mocked_parent_init ____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
__________________________________________________________________________________ test_init_calls_mocked_parent_init_2 ___________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
___________________________________________________________________________________ test_init_calls_real_parent_init_2 ____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Parent __init__ called
Child __init__ called
5 passed in 0.03s

调查结果:

这里我使用了2种方案:

  1. 要么在手动重新分配给 mock 后改回 Parent.__init__ 的原始实现(参见 test_init_calls_updated_parent_init)(不建议)
  2. 或者使用 unittest 和 pytest-mock 的内置补丁功能(参见 test_init_calls_mocked_parent_inittest_init_calls_mocked_parent_init_2

现在,第一个和最后一个测试都正确地调用了实际的 Parent.__init__,即使在进行了所有模拟之后也是如此。