How to Disable Autouse Fixtures in pytest
Learn how to ignore autouse fixtures for one test or more tests
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 responses
.
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 all_chars
.
# 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.seed
to 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
@pytest.fixture(autouse=True)
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 =========================
Disabling an autouse
fixture
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:
@pytest.fixture(autouse=True)
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 random.choices
function.
Let's see what happens when we run the test with this mark...
@pytest.mark.disable_autouse
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 =========================
Conclusion
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:
Learn how to unit test REST APIs in Python with Pytest by example.
7 pytest Features and Plugins That Will Save You Tons of Time
References:
Disable autouse fixtures on specific pytest marks
pytest - is there a way to ignore an autouse fixture?
This article was originally published at https://miguendes.me