64.2 patch as Decorator and Context Manager
Now, let’s talk about patch. It’s arguably the most important tool in the mocking toolbox, and the Python developers, in a rare moment of clarity, gave it two incredibly useful interfaces: a decorator and a context manager. This isn’t just syntactic sugar; it’s a fundamental shift in how you control the scope of your lies, and you should understand both.
The core concept is simple: patch finds the name of an object in a given module and replaces it with a MagicMock (or whatever you tell it to) for the duration of the patch. The magic, and the gotchas, all come from how it performs this sleight of hand and how you control its reach.
The Decorator: Patching for the Entire Party
Use the decorator form when you want your mock to be in effect for the entire lifespan of your test function. It’s the “set it and forget it” approach. You apply it to your test method, and patch handles the setup and teardown for you automatically.
# my_module.py
def expensive_api_call():
return "This costs real money. Don't actually call this in a test."
def my_function():
result = expensive_api_call()
return f"Success: {result}"
# test_my_module.py
from unittest.mock import patch
import my_module
@patch('my_module.expensive_api_call')
def test_my_function_decorator(mock_api_call):
# Arrange: Configure the mock's return value
mock_api_call.return_value = "Fake, cheap data"
# Act
result = my_module.my_function()
# Assert
assert result == "Success: Fake, cheap data"
mock_api_call.assert_called_once_with() # Verify the call happened
Notice the target string: 'my_module.expensive_api_call'. This is where most people’s brains short-circuit. You are not patching the function object itself. You are patching the name expensive_api_call inside the my_module namespace. When my_function() runs and looks for expensive_api_call, it finds my_module.expensive_api_call, which patch has already swapped out for your mock. This is a crucial distinction, especially when dealing with imports. You always patch where an object is used, not necessarily where it is defined.
The mock object is automatically passed as an argument to your test function. The order of the arguments is the order of the decorators, from bottom to top. It’s a bit weird, but you get used to it.
The Context Manager: Surgical Strikes
Sometimes you don’t want the mock for the whole function. Maybe you only need it for one specific call, or you need to test the behavior both with and without the mock in the same test. This is where the context manager form shines. It gives you precise, block-level control.
def test_my_function_context_manager():
# This part runs without the patch
with patch('my_module.expensive_api_call') as mock_api_call:
# This block runs WITH the patch
mock_api_call.return_value = "Fake, cheap data"
result = my_module.my_function()
assert result == "Success: Fake, cheap data"
mock_api_call.assert_called_once()
# And now we're back outside the block, the patch is reverted.
# The real expensive_api_call is restored.
This is infinitely cleaner than the old-school method of manually patching and unpatching. It’s also your best friend for dealing with those “what if the patching fails?” scenarios, because the context manager ensures the patch is always reverted, even if an exception is raised inside the block. It’s basically a try/finally block for your mocks.
Common Pitfalls and The “as” Trick
The most common mistake, by a country mile, is patching the wrong location. Remember: patch where the object is looked up. If your code under test does from requests import get, you patch 'my_module.get', not 'requests.get'. Your module has its own reference to the function, and that’s the one you need to replace.
Both forms allow you to provide a new object to use instead of a auto-created MagicMock. But a more useful pattern is to use the as keyword to capture the mock object for configuration and assertions.
# You can configure the mock right in the decorator...
@patch('my_module.expensive_api_call', return_value="Fake data")
def test_with_config(mock_api_call):
...
# ...or use 'side_effect' for more complex behavior, like raising exceptions.
def mock_behavior():
raise ConnectionError("Network is down!")
@patch('my_module.expensive_api_call', side_effect=mock_behavior)
def test_with_side_effect(mock_api_call):
...
The context manager does this automatically with its as:
with patch('my_module.expensive_api_call') as mock_api:
mock_api.return_value = "Data"
mock_api.side_effect = [1, 2, 3] # Returns these on subsequent calls
...
Why This All Matters
This design isn’t just clever; it’s philosophically aligned with good testing. The decorator encourages you to think about the arrange phase of your test holistically. The context manager forces you to be deliberate about the scope of your test’s influence, preventing mock “leakage” and making your tests more precise and readable. Mastering both gives you the flexibility to write tests that are robust, clear, and—dare I say—elegant. Now go patch something properly.