Decorators are one of Python’s coolest constructs, they are rather simple, yet quite powerful and flexible, they can greatly improve the structure of your code. In my projects, I always shoot for the highest test coverage possible, so naturally, if I have some custom decorators implemented, I always make sure to have some test coverage for them as well. In this article, I show you some of the best practices to test your decorators.
Python decorators can be easily tested by integration tests, but it is good practice to create some unit tests for them as well. You can easily unit test a decorator by applying it to a dummy function or mock. There is a nice trick to “bypass” a decorator as well.
After a quick refresher on decorators, I’ll show you some simple examples of the patterns that I like to use to test them. If you don’t know the difference between integration and unit tests, don’t worry, I’ll also explain it in the following sections.
What Are Python Decorators and How Do They Work? - A Quick Summary
Decorators allow you to extract some common functionality from your methods. The concept is pretty simple, a decorator is a special kind of function, that can take a function as a parameter and return another - wrapped - function. For example:
def triple(func): def wrapper_func(*args, **kwargs): return func(*args, **kwargs) * 3 return wrapper_func
Let’s apply this decorator to a function:
def example(x): return x wrapped = triple(example) example(1) # returns 1 wrapped(1) # returns 3
A decorator is only a shorthand - or syntactic sugar if you prefer - to achieve the same thing in one single step:
@triple def example(x): return x example(1) # returns 3
@wraps Decorator - Accessing The Decorated Function
There is one little issue with the code above though: the wrapped functions metadata is lost. For example:
example.__name__ # returns 'wrapper_func'
There is a nice little utility in
functools, that fixes this problem for us, it’s called
@wraps decorator. We can apply it to the
wrapper_func like so:
from functools import wraps def triple(func): @wraps(func) def wrapper_func(*args, **kwargs): return func(*args, **kwargs) * 3 return wrapper_func
You should always use
wraps, when implementing decorators. It will fix the function metadata (for example: copy the __name__, the docstring, etc).
example.__name__ # returns 'example'
It’ll also set a property named
__wrapped__, which will be a reference to the original, undecorated function. We’ll come back to this later, as this will be important for testing as well.
example._wrapped__ # returns something like `<function example at 0x7fa1a39808c0>` - this is a reference to the original undecorated function
How To Use Decorators? Where Are They Useful?
The short answer is: everywhere :-)
The code above is just a dummy example to demonstrate the syntax and the basic idea behind decorators. In real projects, they tend to contain more complex logic.
Here we’ve just modified the wrapped functions’ return value, but decorators can also be used to introduce side-effects, like logging, or modifying a database record.
Actually, calling the wrapped function is not a requirement at all, you can also wrap the decorated function with conditional logic. This pattern is commonly used for authentication and permission checking.
How To Test A Python Decorator?
Now, that you know that applying a decorator is just a fancy way of executing a function call, it’s pretty easy to write a test for our
class TestTriple(TestCase): def test_returns_3(self): def mock_func(x): return x self.assertEqual(triple(mock_func(1)), 3)
You can even use a lambda, if you prefer:
class TestTriple(TestCase): def test_returns_3(self): self.assertEqual(triple(lambda x:x)(1), 3)
This is quite concise, but it feels a little bit hacky. Good tests also serve as documentation for your code, but if I look at the test above it is not obvious at first glance that we are testing a decorator.
For this reason, if it is possible, I prefer to use the decorator syntax in tests as well:
class TestTriple(TestCase): def test_returns_3(self): @triple def decorated(x): return x self.assertEqual(decorated(1), 3)
The advantage of this approach is more apparent if you have multiple tests. You can pull the function definition up to your
class TestTriple3(TestCase): def setUp(self): @triple def func(x): return x self.decorated = func def test_1_3(self): self.assertEqual(self.decorated(1), 3) def test_2_6(self): self.assertEqual(self.decorated(2), 6)
Another approach is to use a
mock instead of the dummy function. It is especially useful if your logic is a bit more complex. Mocking the decorated function and making the mock return the desired value (or raise an exception) is more simple and results in more easily readable code than implementing some sophisticated logic in your dummy function:
from unittest.mock import MagicMock class TestTriple4(TestCase): def setUp(self): self.mock = MagicMock() self.decorated = triple(self.mock) def test_1_3(self): self.mock.return_value = 1 self.assertEqual(self.decorated(), 3) def test_2_6(self): self.mock.return_value = 2 self.assertEqual(self.decorated(), 6)
How To Test A Decorated Function? How To Disable A Decorator For Testing?
It is quite common that you want to do the opposite - test a decorated function, but do it without running the decorator code.
For example, implementing authentication as a decorator is quite common in web frameworks like Flask or Django. Let’s say we have a function-based Django view, that is only accessible for logged in users:
@login_required def view(): return "logged in"
Making the authentication pass in the test can be pretty tedious, it can require quite a bit of extra code. It takes more time to write your tests, it can lead to some unnecessary code duplication in your tests, and in some cases, it can slow down your test suite. Many times it is better to just skip the authentication.
You cannot actually skip or disable a decorator, but if it uses the above mentioned
@wraps utility, you can access the original function it was applied to via its
Going back to our example with
from functools import wraps def triple(func): @wraps(func) def wrapped_func(*args, **kwargs): return func(*args, **kwargs) * 3 return wrapped_func @triple def example(x): return x
This is how you test it’s return value with and without the decorator:
from unittest import TestCase class TestExample(TestCase): def test_with_the_decorator_returns_3(self): self.assertEqual(example(1), 3) def test_without_the_decorator_returns_1(self): self.assertEqual(example.__wrapped__(1), 1)
Unit Tests vs Integration Tests
So far we’ve been looking at unit tests, meaning that we checked only one piece of software in isolation: either the decorator OR the decorated funciton.
In integration tests our goal is to also test how our software components interact.
Let’s see a quick example of an integration test for a decorator and a decorated function.
Integration Tests for Python Decorators
Writing an integration test is rather straightforward: you can just test your decorator together with the decorated function. It isn’t any different from testing a regular function. Let me demonstrate it using a bit more meaningful example.
This is the decorator that we are going to test:
def log_error(func): def wrapped_func(*args, **kwargs): try: return func(*args, **kwargs) except Exception: logging.exception("There was a problem") return wrapped_func
It wraps the decorated function with a
except block, and if an exception is thrown, just catches it and logs a message.
We are going to apply it to this simple percentage calculator function:
@log_error def percentage(whole, part): return 100 * part / whole
Writing an integration test is quite easy, instead of testing the decorator and the decorated function separately, we just test the wrapped function.
class TestIntegration(TestCase): def test_percentage(self): self.assertAlmostEqual(percentage(4, 1), 75) def test_percentage(self): try: percentage(0, 1) except Exception: self.fail("Unexpected exception")
As you can see, this test case exercises both the
log_error and the
percentage functions and ensures that they work together properly.