Mocking a imported function with pytest [duplicate

2019-05-13 16:28发布

问题:

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