python 使用pytest模拟导入的函数[duplicate]

tp5buhyn  于 2023-03-16  发布在  Python
关注(0)|答案(3)|浏览(136)

此问题在此处已有答案

Python Mocking a function from an imported module(3个答案)
5年前关闭。
我想测试我写的一个邮件发送方法。在文件format_email.py中导入send_email。

from cars.lib.email import send_email

 class CarEmails(object):

    def __init__(self, email_client, config):
        self.email_client = email_client
        self.config = config

    def send_cars_email(self, recipients, input_payload):

在send_汽车_email()中格式化电子邮件内容后,我使用前面导入的方法发送电子邮件。

response_code = send_email(data, self.email_client)

在我的测试文件test_car_email.py中

@pytest.mark.parametrize("test_input,expected_output", test_data)
def test_email_payload_formatting(test_input, expected_output):
    emails = CarsEmails(email_client=MagicMock(), config=config())
    emails.send_email = MagicMock()
    emails.send_cars_email(*test_input)
    emails.send_email.assert_called_with(*expected_output)

当我运行测试时,它在Assert未被调用时失败。我相信问题出在我嘲笑send_email函数的地方。
我应该在哪里模拟这个函数呢?

wxclj1h5

wxclj1h51#

既然您使用的是pytest,我建议您使用pytest内置的'monkeypatch' fixture。
考虑以下简单设置:
我们定义要模拟的函数。

"""`my_library.py` defining 'foo'."""

def foo(*args, **kwargs):
    """Some function that we're going to mock."""
    return args, kwargs

和调用该函数的类放在一个单独的文件中。

"""`my_module` defining MyClass."""
from my_library import foo

class MyClass:
    """Some class used to demonstrate mocking imported functions."""
    def should_call_foo(self, *args, **kwargs):
        return foo(*args, **kwargs)

我们使用“monkeypatch”fixture模拟函数的使用位置

"""`test_my_module.py` testing MyClass from 'my_module.py'"""
from unittest.mock import Mock

import pytest

from my_module import MyClass

def test_mocking_foo(monkeypatch):
    """Mock 'my_module.foo' and test that it was called by the instance of
    MyClass.
    """
    my_mock = Mock()
    monkeypatch.setattr('my_module.foo', my_mock)

    MyClass().should_call_foo(1, 2, a=3, b=4)

    my_mock.assert_called_once_with(1, 2, a=3, b=4)

如果您想重用它,我们还可以将mock分解到它自己的fixture中。

@pytest.fixture
def mocked_foo(monkeypatch):
    """Fixture that will mock 'my_module.foo' and return the mock."""
    my_mock = Mock()
    monkeypatch.setattr('my_module.foo', my_mock)
    return my_mock

def test_mocking_foo_in_fixture(mocked_foo):
    """Using the 'mocked_foo' fixture to test that 'my_module.foo' was called
    by the instance of MyClass."""
    MyClass().should_call_foo(1, 2, a=3, b=4)

    mocked_foo.assert_called_once_with(1, 2, a=3, b=4)
4c8rllxm

4c8rllxm2#

您使用emails.send_email = MagicMock()行模拟的是函数

class CarsEmails:

    def send_email(self):
        ...

这一行只会向emails对象 * 添加 * 一个新函数。然而,这个函数不是从代码中调用的,赋值也不会有任何效果。相反,你应该模拟cars.lib.email模块中的send_email函数。

模拟使用它的函数

一旦你通过from cars.lib.email import send_email将函数send_email导入到你的模块format_email.py中,它就可以用format_email.send_email这个名字了,因为你知道这个函数是在那里被调用的,所以你可以用它的新名字模拟它:

from unittest.mock import patch

from format_email import CarsEmails

@pytest.mark.parametrize("test_input,expected_output", test_data)
def test_email_payload_formatting(config, test_input, expected_output):
    emails = CarsEmails(email_client=MagicMock(), config=config)
    with patch('format_email.send_email') as mocked_send:
        emails.send_cars_email(*test_input)
        mocked_send.assert_called_with(*expected_output)

模拟定义函数的位置

更新

阅读unittest文档中的Where to patch一节确实很有帮助(也可以参见Martijn Pieters的注解):
基本原则是在查找对象的位置打补丁,而查找对象的位置不一定与定义对象的位置相同。
因此,请坚持在使用位置模拟函数,不要从刷新导入或按正确的顺序对齐它们开始,即使可能会出现一些模糊的用例,即由于某种原因无法访问format_email的源代码(就像当它是一个cythonized/compiled C/C++扩展模块时),您仍然只有两种可能的导入方法,因此,只需尝试两种模拟可能性,如在何处打补丁中所述,并使用成功的一种。

原答复

你也可以在send_email的原始模块中模拟它:

with patch('cars.lib.email.send_email') as mocked_send:
    ...

但请注意,如果您在打补丁之前在format_email.py中调用了send_email的导入,则打补丁cars.lib.email不会对format_email中的代码产生任何影响,因为函数已经导入,因此不会调用以下示例中的mocked_send

from format_email import CarsEmails

...

emails = CarsEmails(email_client=MagicMock(), config=config)
with patch('cars.lib.email.send_email') as mocked_send:
    emails.send_cars_email(*test_input)
    mocked_send.assert_called_with(*expected_output)

要解决此问题,您应该在cars.lib.email修补程序之后首次导入format_email

with patch('cars.lib.email.send_email') as mocked_send:
    from format_email import CarsEmails
    emails = CarsEmails(email_client=MagicMock(), config=config)
    emails.send_cars_email(*test_input)
    mocked_send.assert_called_with(*expected_output)

或重新加载模块,例如使用importlib.reload()

import importlib

import format_email

with patch('cars.lib.email.send_email') as mocked_send:
    importlib.reload(format_email)
    emails = format_email.CarsEmails(email_client=MagicMock(), config=config)
    emails.send_cars_email(*test_input)
    mocked_send.assert_called_with(*expected_output)

如果你问我的话,这两种方法都不怎么样,我会坚持在调用函数的模块中模拟函数。

o2gm4chl

o2gm4chl3#

最简单的修复方法如下

@pytest.mark.parametrize("test_input,expected_output", test_data)
def test_email_payload_formatting(test_input, expected_output):
    emails = CarsEmails(email_client=MagicMock(), config=config())
    import format_email
    format_email.send_email = MagicMock()
    emails.send_cars_email(*test_input)
    format_email.send_email.assert_called_with(*expected_output)

基本上,您有一个已经在format_email中导入了send_email的模块,现在必须更新加载的模块。
但这不是最推荐的方法,因为你会失去原来的send_email函数,所以你应该使用上下文补丁,有不同的方法可以做到这一点

方式1

from format_email import CarsEmails

@pytest.mark.parametrize("test_input,expected_output", test_data)
def test_email_payload_formatting(test_input, expected_output):
    emails = CarsEmails(email_client=MagicMock(), config=config())
    with patch('cars.lib.email.send_email') as mocked_send:
        import format_email
        reload(format_email)
        emails.send_cars_email(*test_input)
        mocked_send.assert_called_with(*expected_output)

在这里,我们模拟导入的实际函数

第二条路

with patch('cars.lib.email.send_email') as mocked_send:
    from format_email import CarsEmails

    @pytest.mark.parametrize("test_input,expected_output", test_data)
    def test_email_payload_formatting(test_input, expected_output):
        emails = CarsEmails(email_client=MagicMock(), config=config())
        emails.send_cars_email(*test_input)
        mocked_send.assert_called_with(*expected_output)

这样,文件中的任何测试都可以将修补函数用于其他测试

路3

from format_email import CarsEmails

@pytest.mark.parametrize("test_input,expected_output", test_data)
def test_email_payload_formatting(test_input, expected_output):
    with patch('format_email.send_email') as mocked_send:
        emails = CarsEmails(email_client=MagicMock(), config=config())
        emails.send_cars_email(*test_input)
        mocked_send.assert_called_with(*expected_output)

在这个方法中,我们修补了导入本身,而不是实际被调用的函数。在这种情况下,不需要重载
所以你可以看到有不同的方式来做嘲弄,一些方法来作为良好的做法,一些来作为个人的选择

相关问题