可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
This question already has an answer here:
-
Python Mocking a function from an imported module
2 answers
I would like to test a email sending method I wrote. In file, format_email.py I import 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):
After formatting the email content in send_cars_email() I send the email using the method I imported earlier.
response_code = send_email(data, self.email_client)
in my test file 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)
When I run the test it fails on assertion not called. I believe The issue is where I am mocking the send_email function.
Where should I be mocking this function?
回答1:
What you are mocking with the line emails.send_email = MagicMock()
is the function
class CarsEmails:
def send_email(self):
...
that you don't have. This line will thus only add a new function to your emails
object. However, this function is not called from your code and the assignment will have no effect at all. Instead, you should mock the function send_email
from the cars.lib.email
module.
mocking the function where it is used
Once you have imported the function send_email
via from cars.lib.email import send_email
in your module format_email.py
, it becomes available under the name format_email.send_email
. Since you know the function is called there, you can mock it under its new name:
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)
mocking the function where it is defined
Update:
It really helps to read the section Where to patch in the unittest
docs (also see the comment from Martijn Pieters suggesting it):
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.
So stick with the mocking of the function in usage places and don't start with refreshing the imports or aligning them in correct order. Even when there should be some obscure usecase when the source code of format_email
would be inaccessible for some reason (like when it is a cythonized/compiled C/C++ extension module), you still have only two possible ways of doing the import, so just try out both mocking possibilities as described in Where to patch and use the one that succeeds.
Original answer:
You can also mock send_email
function in its original module:
with patch('cars.lib.email.send_email') as mocked_send:
...
but be aware that if you have called the import of send_email
in format_email.py
before the patching, patching cars.lib.email
won't have any effect on code in format_email
since the function is already imported, so the mocked_send
in the example below won't be called:
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)
To fix that, you should either import format_email
for the first time after the patch of 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)
or reload the module e.g. with 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)
Not that pretty either way, if you ask me. I'd stick with mocking the function in the module where it is called.
回答2:
Since you are using pytest, I would suggest using pytest's
built-in 'monkeypatch' fixture.
Consider this simple setup:
We define the function to be mocked.
"""`my_library.py` defining 'foo'."""
def foo(*args, **kwargs):
"""Some function that we're going to mock."""
return args, kwargs
And in a separate file the class that calls the function.
"""`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)
We mock the function where it is used using the '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)
We could also factor out the mocking into its own fixture if you want to reuse it.
@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)
回答3:
The simplest fix would be below
@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)
Basically you have a module which has already imported send_email
in the format_email
and you have to update the loaded module now.
But it is not the most recommended way of doing it because you loose the original send_email
function. So you should use patch with context. There are different ways of doing that
Way 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)
In this we mock the actual function which was imported
Way 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)
This way any test within your file will used the patched function for other tests also
Way 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)
In this method we patch the import itself and not the actual function which was called. In this case no reload needed as such
So you can see there are different ways of doing mocking, some approaches comes as good practices and some come as personal choice