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)
Benjamin
  • 1,348
  • 2
  • 12
  • 25
  • I would have expected `smtp` and `SMTP` to be the same, since essentially `SMTP`is `smtplib.SMTP` which was supposed to be patched. Patching meaning effectively replaced by a `MagicMock`. – Benjamin Dec 17 '17 at 17:57

1 Answers1

2

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!

Thijs van Dien
  • 6,516
  • 1
  • 29
  • 48
  • 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.](https://stackoverflow.com/q/2461702/5119485) – Benjamin Dec 19 '17 at 10:59
  • @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. – Thijs van Dien Dec 19 '17 at 16:40
  • 1
    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](https://youtu.be/DJtef410XaM). – Thijs van Dien Dec 19 '17 at 18:32
  • 1
    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. – Thijs van Dien Dec 19 '17 at 18:37