pytest is a very robust framework that comes with lots of features.
One such feature is the
autouse fixtures, a.k.a xUnit setup on steroids. They are a special type of fixture that gets invoked automatically, and its main use case is to act as a setup/teardown function.
Another use case is to perform some task, like mocking an external dependency, that must happen before every test.
For example, suppose you have a set of functions that execute HTTP calls. For each one, you provide a test. To ensure your test doesn't call the real API, we can mock the call using a library such
However, if you want one of the tests to call the API, as in an integration test, then you'll have to disable the
autouse fixture. And that's what we're going to see today.
In this post, we'll learn a simple technique to disable
autouse fixtures for one or more tests.
Table of Contents
pytest Fixture Autouse - Example
In this section, we'll build an example to illustrate the usage of autouse fixtures and how to we can disable them when necessary.
For this example, we'll write some tests that mock the random module.
Consider the following case where we'll be building a random password generator. The function takes a password length and returns a random string of size length. And to do that, it uses
random.choices to randomly pick
k chars from a seed string called
# file: autouse/__init__.py import random import string def get_random_password(length: int = 20) -> str: """ Generates a random password with up to length chars. """ all_chars = string.ascii_letters + string.digits + string.punctuation return ''.join(random.choices(all_chars, k=length))
Since we don't control how
random.choices picks, we cannot test it in a deterministic way. To make that happen, we can patch
random.choices and make it return a fixed list of chars.
You can also set the
random.seedto a fixed number before every test run by making it an autouse fixture.
# file: tests/test_random.py import random import pytest from autouse import get_random_password def patch_random(): with unittest.mock.patch('autouse.random.choices') as mocked_choices: mocked_choices.return_value = ['a', 'B', 'c', '2'] yield def test_mocked_random_char(): assert get_random_password() == 'aBc2'
The benefits of the
autouse fixture are that we don't need to pass it to every test that needs it. And by using
yield, you undo the patching after the test finishes, which is great for cleaning up.
If we run this test, it passes just fine.
============================= test session starts ============================== collecting ... collected 1 item test_random.py::test_mocked_random_char PASSED [100%] ========================= 1 passed, 1 warning in 0.05s =========================
Now, let's say that we want to test the robustness of our random number generator and we want to test that it never generates the same string in a row.
To do that, we need to call the real function, and not patch it. Let's create this test and see what it does.
# file: tests/test_random.py def test_random_char_does_not_duplicate(): password_one = get_random_password() password_two = get_random_password() assert password_one != password_two
But when we run this test, it fails:
test_random.py::test_random_char_does_not_duplicate FAILED [100%] test_random.py:18 (test_random_char_does_not_duplicate) def test_random_char_does_not_duplicate(): password_one = get_random_password() password_two = get_random_password() > assert password_one != password_two E AssertionError: assert 'aBc2' != 'aBc2' test_random.py:22: AssertionError
The reason is that
pytest injects the
autouse fixture to every test case within the scope you specified.
Now the question is, how can we disable an
autouse fixture for one or more tests in pytest?
One way to do that is to create a custom
pytest mark and annotate the test with it. For example:
def patch_random(request): if 'disable_autouse' in request.keywords: yield else: with unittest.mock.patch('autouse.random.choices') as mocked_choices: mocked_choices.return_value = ['a', 'B', 'c', '2'] yield
In this example, we created a
pytest mark called
disable_autouse and we annotated the
test_random_char_does_not_duplicate test with it.
This mark becomes available in the request fixture. We can pass this
request fixture to the
autouse one and check if the keyword
disable_autouse is in the list of keywords.
When that's the case, we don't mock, just
yield, which gives back the control to
test_random_char_does_not_duplicate, thus avoiding mocking the
Let's see what happens when we run the test with this mark...
def test_random_char_does_not_duplicate(): password_one = get_random_password() password_two = get_random_password() assert password_one != password_two
The test passes, since it's not mocked anymore.
============================= test session starts ============================== collecting ... collected 1 item test_random.py::test_random_char_does_not_duplicate PASSED [100%] ========================= 1 passed, 1 warning in 0.03s =========================
pytest has some great features such as
autouse fixture. They make it easier to set up and teardown unit tests but if we ever want to disable it, then things get trickier.
In this post, we learned how to disable autouse fixture in pytest by marking the tests with a custom pytest mark. I hope you enjoyed this article and see you next time.
Other posts you may like:
This article was originally published at https://miguendes.me