How to Disable Autouse Fixtures in pytest

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

  1. pytest's autouse fixture - example
  2. Disabling an autouse fixture
  3. Conclusion

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 =========================

pytest autouse fixture being injected inside a test

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:

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