用pytest模拟一个导入的函数

Mocking a imported function with pytest

我想测试一下我写的一个邮件发送方法。在文件中,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_cars_email() 中格式化电子邮件内容后,我使用之前导入的方法发送电子邮件。

 response_code = send_email(data, self.email_client)

在我的测试文件中 test_car_emails.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)

当我 运行 测试时,它因未调用断言而失败。我相信问题出在我嘲笑 send_email 函数的地方。

我应该在哪里模拟这个函数?

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

class CarsEmails:

    def send_email(self):
        ...

你没有。因此,该行只会 添加 一个新功能到您的 emails 对象。但是,此函数不会从您的代码中调用,赋值将完全无效。相反,您应该模拟 cars.lib.email 模块中的函数 send_email

在使用它的地方模拟函数

在您的模块 format_email.py 中通过 from cars.lib.email import send_email 导入函数 send_email 后,它就可以在名称 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)

在定义的地方模拟函数

更新:

阅读建议的 Where to patch in the unittest docs (also see the from Martijn Pieters 部分确实很有帮助):

The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.

所以坚持在使用地方模拟函数,不要从刷新导入或按正确顺序对齐它们开始。即使当 format_email 的源代码由于某种原因无法访问时应该有一些模糊的用例(比如当它是 cythonized/compiled C/C++ 扩展模块时),你仍然只有两种可能的导入方式,所以只需尝试 Where to patch 中描述的两种模拟可能性,然后使用成功的一种。

原回答:

您还可以在其原始模块中模拟 send_email 函数:

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

但请注意,如果您在打补丁之前在 format_email.py 中调用了 send_email 的导入,则打补丁 cars.lib.email 不会对 [=25= 中的代码产生任何影响] 因为函数已经被导入,所以下例中的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)

如果你问我的话,无论哪种方式都不是那么漂亮。我会坚持在调用它的模块中模拟函数。

最简单的修复方法如下

@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 功能。所以你应该使用带有上下文的补丁。有不同的方法可以做到这一点

方式一

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)

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

方式 2

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)

这样,您文件中的任何测试都将对其他测试使用补丁函数

方式三

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)

在这个方法中,我们修补了导入本身,而不是调用的实际函数。在这种情况下不需要重新加载

所以你可以看到有不同的方式来进行模拟,一些方法是好的做法,一些是个人选择

既然你使用的是pytest,我建议使用pytest的 built-in 'monkeypatch' 灯具。

考虑这个简单的设置:

我们定义了被模拟的函数。

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


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

并在单独的文件中调用该函数的 class。

"""`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' 夹具模拟函数 在使用它的地方

"""`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)

如果您想重用它,我们也可以将模拟分解到它自己的固定装置中。

@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)