使用pytest模拟导入的函数

时间:2018-02-15 16:36:51

标签: python mocking pytest magicmock

我想测试一下我写的电子邮件发送方法。在文件中,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函数。

我应该在哪里嘲笑这个功能?

3 个答案:

答案 0 :(得分:4)

你用emails.send_email = MagicMock()行嘲笑的是函数

class CarsEmails:

    def send_email(self):
        ...

你没有。因此,此行仅向您的emails对象添加新函数。但是,不会从代码中调用此函数,并且赋值根本不起作用。相反,您应该从send_email模块中模拟函数cars.lib.email

模拟使用它的函数

在您的模块send_email中通过from cars.lib.email import 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部分非常有帮助(另请参阅comment建议的Martijn Pieters):

  

基本原则是你可以在查找对象的位置进行修补,这不一定与定义对象的位置相同。

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

原始回答

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

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

但请注意,如果您在修补前调用send_emailformat_email.py的导入,则修补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)

要解决此问题,您应该在format_email补丁后第一次导入cars.lib.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)

如果你问我,不是那么漂亮。我一直坚持在模块中模拟函数。

答案 1 :(得分:1)

由于您使用的是pytest,我建议使用pytest 内置'monkeypatch'灯具。

考虑这个简单的设置:

我们定义要模拟的函数。

"""`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'夹具模拟使用它的位置

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

答案 2 :(得分:-1)

最简单的修复方法是

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

基本上你有一个已在send_email中导入format_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)

在此我们模拟导入的实际功能

方式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)

这样,文件中的任何测试都将使用修补函数进行其他测试

方式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)

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

所以你可以看到有不同的方法进行嘲弄,有些方法是好的做法,有些是个人选择