我想测试我写的电子邮件发送方法。在文件中,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函数的地方。
我应该在哪里嘲笑这个函数?
由于您使用的是pytest,因此我建议使用pytest的 内置"猴子补丁"夹具。
考虑这个简单的设置:
我们定义要模拟的函数。
"""`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)
我们使用"猴子补丁"夹具模拟使用它的功能
"""`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)
你用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)
模拟定义函数的位置
更新:
阅读unittest
文档中的"在哪里修补"部分确实很有帮助(另请参阅Martijn Pieters的建议):
基本原则是修补查找对象的位置,该位置不一定与定义对象的位置相同。
因此,坚持在使用的地方嘲笑函数,不要从刷新导入或按正确的顺序对齐它们开始。即使应该有一些晦涩的用例,当format_email
的源代码由于某种原因无法访问时(例如当它是一个cythonized/compile 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)
如果你问我,无论哪种方式都不是那么漂亮。我会坚持在调用它的模块中嘲笑该函数。
最简单的修复如下
@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)
在此,我们模拟导入的实际函数
方式 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)
在此方法中,我们修补导入本身,而不是调用的实际函数。在这种情况下,不需要重新加载
所以你可以看到有不同的嘲笑方式,有些方法是好的做法,有些是个人选择。