patch: replacing dependencies

unittest · Advanced · 13 min

Most code calls its dependencies directly, so patching is how you mock them without changing the code.

patch("module.name") replaces an object for the duration of a test and restores it afterwards.

with patch("myapp.billing.requests.post") as post:
    post.return_value.status_code = 200
    charge(10)

As a decorator, @patch("target") passes the mock as an extra argument; stacked decorators pass mocks bottom-up. patch.object(obj, "attr") patches an attribute of an object you already have, and patch.dict(os.environ, {"MODE": "test"}) patches a dictionary.

The rule that trips everyone: patch where the name is looked up, not where it's defined. If myapp/billing.py does from datetime import datetime and calls datetime.now(), patch myapp.billing.datetime, because billing holds its own reference. Patching datetime.datetime has no effect there (and fails anyway: built-in types can't be patched). If the module does import requests and calls requests.post, patching requests.post works because the lookup happens through the module at call time.

Useful options: return_value=, side_effect=, new= to provide a specific replacement, and autospec=True (next level). In setUp, use self.enterContext(patch(...)) so the patch is undone automatically.

Example

import unittest
from unittest.mock import patch

# --- code under test (normally in another module) ---
from time import monotonic          # imported name: patch it HERE, in this module
def elapsed_label(start):
    secs = monotonic() - start
    return "slow" if secs > 1 else "fast"
# ----------------------------------------------------

class TestElapsed(unittest.TestCase):
    @patch(f"{__name__}.monotonic", return_value=105.0)
    def test_slow(self, fake_monotonic):
        self.assertEqual(elapsed_label(100.0), "slow")
        fake_monotonic.assert_called_once()

    def test_wrong_place(self):
        with patch("time.monotonic", return_value=105.0) as fake:
            elapsed_label(100.0)
            fake.assert_not_called()    # our module kept its own reference

unittest.main(exit=False)           # Ran 2 tests ... OK

Remember

  • patch replaces an object for one test and restores it
  • Patch where the name is looked up, not where it's defined
  • patch.object and patch.dict cover attributes and dictionaries

Watch out for

  • Patching 'requests.get' when the module did 'from requests import get'; patch 'mymodule.get' instead.

Check yourself

  1. app/report.py has 'from os import getcwd' and calls getcwd(). What should a test patch?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/unittest.mock.html#where-to-patch