2025, Sep 27 09:00
How to Unit Test Conditional Python Decorators: Mock the Wrapped Function, Not the Decorator
Unit test conditional Python decorators correctly: avoid definition-time assertions, mock the wrapped function, and verify runtime behavior with unittest.mock
Testing conditionally applied decorators in Python can be deceptively tricky. You may instrument the decorator and count calls, only to discover that your assertion never captures the actual execution path. The core pitfall is simple: decorators run at definition time, while the function they return runs at call time. If you want to assert behavior under a condition, you need to observe the right layer.
The setup
Consider a nested decorator where an outer wrapper is applied only when a predicate evaluates to True. The logic below wires a parent wrapper and switches execution depending on the condition.
# decoratorFactory.py
def bootstrap_guard(func):
    print("parent decorator is initialized")
    primed = False
    def call_through(*args, **kwargs):
        nonlocal primed
        if primed:
            return func(*args, **kwargs)
        else:
            primed = True
            res = func(*args, **kwargs)
            return res
    return call_through
def select_wrap(predicate):
    def apply_guard(fn):
        guarded_fn = bootstrap_guard(fn)
        def envelope(*args, **kwargs):
            if predicate():
                print(f"{predicate()}")
                return guarded_fn(*args, **kwargs)
            else:
                print(f"{predicate()}")
                return fn(*args, **kwargs)
        return envelope
    return apply_guard
A straightforward but misleading unit test would attempt to patch the decorator and assert its call count. That number, however, reflects decoration time, not whether the wrapped function ran during invocation.
import unittest
from unittest import mock
import util
from decoratorFactory import select_wrap
class TestGateDecorator(unittest.TestCase):
    @mock.patch("decoratorFactory.bootstrap_guard")
    def test_select_wrap_decorates_once(self, mock_guard):
        mock_guard.return_value = lambda: "Hello"
        with mock.patch("util.flag_ready", return_value=True):
            @select_wrap(predicate=util.flag_ready)
            def inner():
                return "Hello"
            self.assertEqual(inner(), "Hello")
            self.assertEqual(mock_guard.call_count, 1)
Even if you toggle the condition to False, that assertion still passes because the decorator is applied exactly once at definition time. It says nothing about which callable ran when the function was executed.
Why the assertion is misleading
When you patch the decorator and read its call count, you only observe the decoration phase. The function returned by the decorator is the one that actually executes when you call the decorated function. If your condition routes execution through the parent wrapper only sometimes, you must track the returned wrapped function to verify runtime behavior.
The right way to test
The fix is to mock the wrapped function returned by the parent decorator and assert calls against that object. When the condition is True, your conditional decorator should call the parent’s wrapper; when the condition is False, it should bypass it and call the original function directly. The tests below demonstrate exactly that.
import unittest
from unittest import mock
import util
from decoratorFactory import select_wrap
class TestGateDecorator(unittest.TestCase):
    def test_select_wrap_executes_parent_when_true(self):
        wrapped_spy = mock.MagicMock(return_value="Hello from parent")
        with mock.patch("decoratorFactory.bootstrap_guard") as patched_guard:
            patched_guard.return_value = wrapped_spy
            with mock.patch("util.flag_ready", return_value=True):
                @select_wrap(predicate=util.flag_ready)
                def inner():
                    return "Hello from original"
                result = inner()
                wrapped_spy.assert_called_once()
                self.assertEqual(result, "Hello from parent")
    def test_select_wrap_bypasses_parent_when_false(self):
        wrapped_spy = mock.MagicMock(return_value="Hello from parent")
        with mock.patch("decoratorFactory.bootstrap_guard") as patched_guard:
            patched_guard.return_value = wrapped_spy
            with mock.patch("util.flag_ready", return_value=False):
                @select_wrap(predicate=util.flag_ready)
                def inner():
                    return "Hello from original"
                result = inner()
                wrapped_spy.assert_not_called()
                self.assertEqual(result, "Hello from original")
This approach captures the two critical facts. First, patching the decorator function itself only proves that decoration happened, which is always once. Second, patching and tracking the specific wrapped callable proves what executed at runtime, which is exactly what the conditional flow controls.
Why this matters
Decorator testing often blurs two moments in time: decoration and invocation. In complex codebases—in particular, those using layered decorators or feature flags—asserting the wrong moment leads to green tests that do not validate actual behavior. Aligning tests with runtime execution ensures you catch regressions in the contract the decorator enforces, not just that the decorator was applied.
Takeaways
When a decorator decides at call time whether to route into a parent wrapper or fall back to the original function, validate the execution path by asserting calls on the wrapped function returned by the parent decorator. Keep your assertions focused on what runs, not just what gets attached during definition. This small shift makes tests resilient and honest about the behavior you care about.
The article is based on a question from StackOverflow by Rohit Pathak and an answer by Favor.