用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)
我想测试一下我写的一个邮件发送方法。在文件中,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
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)