4

I want to mock out the generation of an SMTP client form smtplib. The following code:

from smtplib import SMTP
from unittest.mock import patch

with patch('smtplib.SMTP') as smtp:
    print(SMTP, smtp)

returns

<class 'smtplib.SMTP'> <MagicMock name='SMTP' id='140024329860320'>

implying that the patch failed.

EDIT: Interestingly Monkey Patching as described here gives the same result.

import smtplib
from smtplib import SMTP
from unittest.mock import MagicMock

smtp = MagicMock()
smtplib.SMTP = smtp
print(SMTP, smtp)
1
  • I would have expected smtp and SMTP to be the same, since essentially SMTPis smtplib.SMTP which was supposed to be patched. Patching meaning effectively replaced by a MagicMock. Commented Dec 17, 2017 at 17:57

1 Answer 1

3

I hardly do any patching, but I believe you're patching either too late, or the wrong thing. SMTP has already been imported, resulting in a direct reference to the original class—it will not be looked up in smtplib anymore. Instead, you'd need to patch that reference. Let's use a more realistic example, in which you have module.py and test_module.py.

module.py:

import smtplib
from smtplib import SMTP # Basically a local variable

def get_smtp_unqualified():
    return SMTP # Doing a lookup in this module

def get_smtp_qualified():
    return smtplib.SMTP # Doing a lookup in smtplib

test_module.py

import unittest
from unittest import patch
from module import get_smtp_unqualified, get_smtp_qualified

class ModuleTest(unittest.TestCase):
    def test_get_smtp_unqualified(self):
        with patch('module.SMTP') as smtp:
            self.assertIs(smtp, get_smtp_unqualified())

    def test_get_smtp_qualified_local(self):
        with patch('module.smtplib.SMTP') as smtp:
            self.assertIs(smtp, get_smtp_qualified())

    def test_get_smtp_qualified_global(self):
        with patch('smtplib.SMTP') as smtp:
            self.assertIs(smtp, get_smtp_qualified())

As long as you patch in time before a lookup, it does what you want—3 passing tests. The very earliest time would be before importing any other modules than unittest. Then those modules will not have imported smtplib.SMTP yet. More on that here. It gets tricky though, when your tests are split over multiple modules.

Patching is inherently dirty. You're messing with another's internals. To make it work, you have to look on the inside. If the inside changes, tests will break. That's why you should consider it a last resort and prefer different means, such as dependency injection. That's a whole different topic, but in any case, don't rely on patching to prevent messages from going out—also change the configuration!

Sign up to request clarification or add additional context in comments.

4 Comments

This is probably another question altogether, but do you have any pointers on how to accomplish this task with DI in Python. The community doesn't seem to agree how to do DI/IoC.
@Benjamin It's a big topic. The key is to keep it simple, so I agree with no IoC container ("poor man's DI"). If code as part of its job needs to send emails, don't let it instantiate SMTP but pass in a configured instance, real in production, fake when unit testing. Even better, abstract the action and limit the interface, e.g. make an EmailSender class that has just one method taking an address, subject and body. The production version will use SMTP, but the fake version is trivial to make even without MagicMock. Testing EmailSender will be different from testing code that uses it.
It's "infrastructure" that sits between your business logic and the rest of the world. Test just that small layer with integrated tests (letting it talk to a dummy SMTP server) or just manually whenever you touch it. You'll get a tree of objects to be assembled at a root, e.g. a request handler. That's where you do the instantiations, e.g. ThingThatSendsEmail(EmailSender(ConfigProvider())). A container just turns that manual work into a more declarative format. Another (perhaps more pragmatic) option is best demonstrated in The Clean Architecture in Python.
I must add that the way DI is presented in that talk is quite misleading, like the example of having to pass more and more in the higher up you go. It can be solved by distributing responsibilities better and using objects with dependencies injected through __init__ rather than functions. Still, the alternative is valid. In any case, that's about as much as I can fit in a few comments. My most important advice would be not to limit yourself to the Python world or any specific technologies, but focus on principles and then find your favorite way of applying those.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.