Programming is the art of adding bugs to an empty text file.
Mastering unittest.mock
capabilities of Python to prevent these bugs, is another art of its own. And one of the trickiest move of them all is how to mock a decorator, such as it doesn't go in the way of the function you want to test.
Let's consider the following function to test:
### module api.service
from utils.decorators import decorator_in_the_way
@decorator_in_the_way
def function_to_be_tested(*args, **kwargs):
# do something
...
The problem here is to write a unit test for function_to_be_tested
without invoking the decorator (which would make your test fail)
Problem: it's not possible to @patch
the decorator above the test function, as it is too late: in Python functions are decorated at module-loading time. So this:
### file service_test.py
from unittest.mock import patch
from .service import function_to_be_tested
def mock_decorator = ...
@patch("api.service.decorator_in_the_way", mock_decorator)
def test_function_to_be_tested():
result = function_to_be_tested()
assert ...
... simply does not work. The patched decorator will simply be ignored, as the function to be tested already has been instrumented with the original one.
Fortunately, there is a workaround. The idea is to patch the decorator before the module to test is loaded. Of course the requirement is that the decorator and the function to test don't belong to the same module
The following example will work:
### file service_test.py
from unittest.mock import patch
from functools import wraps
def mock_decorator(*args, **kwargs):
"""Decorate by doing nothing."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(*args, **kwargs)
return decorated_function
return decorator
# PATCH THE DECORATOR HERE
patch('utils.decorators.decorator_in_the_way', mock_decorator).start()
# THEN LOAD THE SERVICE MODULE
from .service import function_to_be_tested
# ALL TESTS OF THE TEST SESSION WILL USE THE PATCHED DECORATOR
def test_function_to_be_tested():
result = function_to_be_tested() # uses the mock decorator
assert ...
Hope this helps !
Iām Matthieu, data engineer at Stack Labs.
If you want to join an enthousiast Data Engineering or Cloud Developer team, please contact us.
Top comments (4)
Helpful. In my case, I want to mock an external decorator like
from third_party import decorator
.You can move import statements to another module, then import that decorator at place u are going to use. So u are able to mock it. Decorator help alot, but also annoy when testing.
Nice article, thanks!
How to be if we don't want to mock at a session level? When do we need to stop the patcher if we want to apply the patcher at a module level?
Indeed, this method will patch all decorators of the testing session. If you want to patch only a module, you will have to
stop()
the patch after the test, and don't forget to reload the .services module to restore the true decorated functions.Nice one!!