python - 如何恢复模拟

标签 python mocking pytest python-unittest

当我弄清楚为什么我的测试文件运行顺利但如果使用 pytest 对整个包运行测试时就会中断,我几乎要疯了。

我有一个父类和一个子类。子进程调用父进程的 init,如下所示:

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__ 始终保持模拟

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

我应该创建一些pytest设置/拆卸或者什么是可以接受的方法来防止这种情况?

最佳答案

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

我的建议是使用 unittest 中已有的修补功能,而不是手动将 Parent.__init__ 的实现更改为 Mock。和 pytest-mock .

  • 我们还可以使用monkeypatch正如@MattSom 所建议的(参见评论部分)

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

调查结果:

这里我使用了两种解决方案:

  1. 手动重新分配为模拟后,更改回 Parent.__init__ 的原始实现(请参阅 test_init_calls_updated_pa​​rent_init)(不建议)
  2. 或者使用 unittest 和 pytest-mock 的内置修补功能(请参阅 test_init_calls_mocked_pa​​rent_inittest_init_calls_mocked_pa​​rent_init_2)

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

关于python - 如何恢复模拟,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68576703/

相关文章:

django - pytest,如何在测试之间保持数据库更改

python - Pytest HTML 报告 : how to get the name of the report file?

python - Tkinter 百分比大小?

python - numba.vectorize - 不支持的数组数据类型

unit-testing - 测试时模拟 Gradle 插件使用的类

python - 为什么 caplog.text 是空的,即使我正在测试的函数正在记录?

python - 从列表创建 DataFrame

python - 如何通过 AJAX 向 Flask 发送数据?

.net - 模拟 WCF 客户端代理的最佳方法

java - 如何在 Spock 中模拟 HttpServletRequest