Testing functions in Python that have a decorator applied to them is not ideal as the tests have to be written to take into account the decorator. The testing guard decorator can help remove the problem by selectively running the function with or without the decorator by detecting the environment in which it is run. When you make the decorator a part of your project you might like to write tests for the testing guard decorator itself.
Testing Guard Decorator
As a reminder, the following is the decorator code for the testing guard.
# testing_guard.py
"""Demonstrates a guard decorator."""
import os
def testing_guard(decorator_func):
"""
Decorator that only applies another decorator if the appropriate
environment variable is not set.
Args:
decorator_func: The function that applies the decorator.
Returns:
Function that dynamically decides whether to apply the decorator based
on the environment.
"""
def replacement(original_func):
"""
Function that is used as the decorator function instead of the
decorator function.
Args:
original_func: The function being decorated.
Returns:
Function that dynamically picks between the original and
decorated function depending on the environment.
"""
# Creating decorated function
decorated_func = decorator_func(original_func)
def apply_guard(*args, **kwargs):
"""
Dynamically picks between decorated and original function based on
environment.
"""
if os.getenv('TESTING') is not None:
# Use original function
return original_func(*args, **kwargs)
# Use decorated function
return decorated_func(*args, **kwargs)
return apply_guard
return replacement
The key pieces of functionality are on lines 39-43. Line 39 is the check for whether the function is being executed in the testing environment. If it is, the original function is called without the decorator on line 41. If it isn’t, then the decorated function is called on line 43. The key reason that the decorator works is that it gets access to both the decorator and original function and that it gets to run code every time the decorated function is run.
Testing the Testing Guard Decorator
There are 2 scenarios to test. The first is that the decorator is executed in the test environment, and the second is when it is executed in the non-test environment. In both cases we care about both that the correct function is called with the correct arguments (and that the other function is not called) and that the return value from the correct function is returned.
Let’s first define 2 fixtures that will help during testing. We will repeatedly need some generic *args and **kwargs and the particular value we choose for either doesn’t add anything to help understand the tests themselves so it is better to hide that detail in a fixture.
@pytest.fixture(scope='session')
def args():
"""Generates function *args"""
return ('arg1', 'arg2')
@pytest.fixture(scope='session')
def kwargs():
"""Generates function **kwargs"""
return {'kwargs1': 'kwarg1', 'kwarg2': 'kwarg2'}
Test Environment
When the decorator is executed in the test environment, it should call the original function and return the return value of the original function. It should not call the decorator return value. To focus the tests, let’s split those into three separate tests.
The first test just checks that the decorator return value is not called:
def test_testing_guard_set_decorated_call(monkeypatch):
"""
GIVEN TESTING environment variable is set and mock decorator
WHEN decorator is applied to a function after decorating it with the
testing guard and calling the decorated function
THEN decorator return value is not called.
"""
# Setting TESTING environment variable
monkeypatch.setenv('TESTING', '')
# Defining mock decorator
mock_decorator = mock.MagicMock()
# Decorating with testing guard and calling
guarded_mock_decorator = testing_guard(mock_decorator)
# Applying decorator
mock_decorated_func = guarded_mock_decorator(mock.MagicMock())
# Calling function
mock_decorated_func()
# Checking decorator call
mock_decorator.return_value.assert_not_called()
To understand the test, some understanding of Python decorators is required. A decorator is another function whose return value replaces the function it is decorating. A decorator could, for example, return the print function and then the function being decorator would never be called. In most cases, however, the function being decorator is called in the body of the function the decorator returns.
On line 9 of the test the testing environment is setup. On line 11 a mock function that will serve as the decorator to which the testing guard is applied is defined. On line 14 the testing guard is applied to the mock decorator. Then a mock function is decorated with guarded mock decorator on line 16 and the decorated function is called on line 18. On line 21 it is checked that the return value of the decorator, which would usually be called instead of the original function, is not called since, in the testing environment, the original function should always be called.
The next test checks that the original function is called with the correct arguments. For this we will ned the args and kwargs fixtures.
def test_testing_guard_set_func_call(monkeypatch, args, kwargs):
"""
GIVEN TESTING environment variable is set, mock function and args and
kwargs
WHEN a decorator is applied to the function after decorating it with the
testing guard and calling the decorated function with args and kwargs
THEN function is called with args and kwargs.
"""
# Setting TESTING environment variable
monkeypatch.setenv('TESTING', '')
# Defining mock decorator
mock_func = mock.MagicMock()
# Decorating with testing guard and calling
mock_decorator = mock.MagicMock()
guarded_mock_decorator = testing_guard(mock_decorator)
# Applying decorator
mock_decorated_func = guarded_mock_decorator(mock_func)
# Calling function
mock_decorated_func(*args, **kwargs)
# Checking decorator call
mock_func.assert_called_once_with(*args, **kwargs)
This tests is very similar to the first test but, instead of keeping track of the decorator, the original function is defined on line 12. The procedure on lines 14-20 is very similar to the first test with the only difference being that the mock decorated function is called with the args and kwargs fixtures. On line 23 the original function call is checked.
The final test checks the return value of the mock decorated function call. It is much like the previous test, except that it doesn’t pass in any args nor kwargs.
def test_testing_guard_set_return(monkeypatch):
"""
GIVEN TESTING environment variable is set and mock function
WHEN a decorator is applied to the function after decorating it with the
testing guard and calling the decorated function
THEN the return value is the function return value.
"""
# Setting TESTING environment variable
monkeypatch.setenv('TESTING', '')
# Defining mock function
mock_func = mock.MagicMock()
# Decorating with testing guard and calling
mock_decorator = mock.MagicMock()
guarded_mock_decorator = testing_guard(mock_decorator)
# Applying decorator
mock_decorated_func = guarded_mock_decorator(mock_func)
# Calling function
return_value = mock_decorated_func()
# Checking decorator call
assert return_value == mock_func.return_value
Non-Test Environment
The second series of tests are outside the test environment. Since the tests for the testing guard are likely running inside the test environment, it is usually best to deactivate the testing environment as a part of the test as you may want to have the testing environment active by default during your tests.
The tests are very similar to the tests in the test environment. The difference is that now the decorator return value should be called, the original function should not be called and the return value of the decorator return value should be returned. The tests are shown below.
def test_testing_guard_not_set_decorated_call(monkeypatch, args, kwargs):
"""
GIVEN TESTING environment variable is not set, mock decorator and args and
kwargs
WHEN decorator is applied to a function after decorating it with the
testing guard and calling the decorated function with args and kwargs
THEN decorator return value is called with args and kwargs.
"""
# Removing TESTING environment variable
monkeypatch.delenv('TESTING', raising=False)
# Defining mock decorator
mock_decorator = mock.MagicMock()
# Decorating with testing guard and calling
guarded_mock_decorator = testing_guard(mock_decorator)
# Applying decorator
mock_decorated_func = guarded_mock_decorator(mock.MagicMock())
# Calling function
mock_decorated_func(*args, **kwargs)
# Checking decorator call
mock_decorator.return_value.assert_called_once_with(*args, **kwargs)
def test_testing_guard_not_set_return(monkeypatch):
"""
GIVEN TESTING environment variable is not set and mock decorator
WHEN decorator is applied to a function after decorating it with the
testing guard and calling the decorated function
THEN the return value is the decorator's return value return value.
"""
# Removing TESTING environment variable
monkeypatch.delenv('TESTING', raising=False)
# Defining mock decorator
mock_decorator = mock.MagicMock()
# Decorating with testing guard and calling
guarded_mock_decorator = testing_guard(mock_decorator)
# Applying decorator
mock_decorated_func = guarded_mock_decorator(mock.MagicMock())
# Calling function
return_value = mock_decorated_func()
# Checking decorator call
assert return_value == mock_decorator.return_value.return_value
def test_testing_guard_not_set_func_call(monkeypatch):
"""
GIVEN TESTING environment variable is not set and mock function
WHEN a decorator is applied to the function after decorating it with the
testing guard and calling the decorated function
THEN function is not called.
"""
# Removing TESTING environment variable
monkeypatch.delenv('TESTING', raising=False)
# Defining mock decorator
mock_func = mock.MagicMock()
# Decorating with testing guard and calling
mock_decorator = mock.MagicMock()
guarded_mock_decorator = testing_guard(mock_decorator)
# Applying decorator
mock_decorated_func = guarded_mock_decorator(mock_func)
# Calling function
mock_decorated_func()
# Checking decorator call
mock_func.assert_not_called()
This demonstrates how to test the testing guard. The setup and teardown of your test environment might be more complicated in which case your tests would have to reflect that.