Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking a imported function with pytest [duplicate]

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?

like image 558
user7692855 Avatar asked Dec 11 '22 07:12

user7692855


2 Answers

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)
like image 137
NSteinhoff Avatar answered Dec 21 '22 01:12

NSteinhoff


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.

like image 24
hoefling Avatar answered Dec 21 '22 01:12

hoefling