How to Use Fixtures as Arguments in pytest.mark.parametrize

How to Use Fixtures as Arguments in pytest.mark.parametrize

Learn how to use pytest mark parametrize with fixtures

TL;DR

Time is a precious resource so I won't waste yours. In this post, you'll learn how to use a pytest fixture in parametrize using a library or getfixturevalue.

Introduction

In this post, we'll see how we can use pytest.mark.parametrize with fixtures. This is a long-wanted feature that dates back to 2013. Even though pytest doesn't support it yet, you'll see that we can actually make it happen.

Problem

You want to pass a fixture to parametrize.

Suppose that you have a simple function called is_even(n) that returns true if n is divisible by 2. Then you create a simple test for it that receives a fixture named two that returns 2. To make the test more robust, you set up another fixture named four that returns 4. Now you have two individual tests, as illustrated below.

Implementation:

def is_even(n: int) -> bool:
    """Returns True if n is even."""
    return n % 2 == 0

Tests:

@pytest.fixture()
def two():
    return 2

@pytest.fixture()
def four():
    return 4

def test_four_is_even(four):
    """Asserts that four is even"""
    assert is_even(four)

def test_two_is_even(two):
    """Asserts that two is even"""
    assert is_even(two)

If we run these tests, they pass, which is good. Even though you’re quite happy with the outcome, you need to test one more thing. You want to assert that the multiplication of an even number by and odd one produces an even result. To accomplish that, you create two more fixtures, one and three. You plan to use them as arguments in a parameterized test, like so:

@pytest.fixture()
def one():
    return 1

@pytest.fixture()
def three():
    return 3

@pytest.mark.parametrize(
    "a, b",
    [
        (one, four),
        (two, three),
    ],
)
def test_multiply_is_even(a, b):
    """Assert that an odd number times even is even."""
    assert is_even(a * b)

When we run this test, we get the following output:

_______________________ test_multiply_is_even[two-three] _______________________

a = <function two at 0x7f9d862ee790>, b = <function three at 0x7f9d862eedc0>

    @pytest.mark.parametrize(
        "a, b",
        [
            (one, four),
            (two, three),
        ],
    )
    def test_multiply_is_even(a, b):
        """Assert that an odd number times even is even."""
>       assert is_even(a * b)
E       TypeError: unsupported operand type(s) for *: 'function' and 'function'

tests/test_variables.py:71: TypeError
=========================== short test summary info ============================
FAILED tests/test_variables.py::test_multiply_is_even[one-four] - TypeError: ...
FAILED tests/test_variables.py::test_multiply_is_even[two-three] - TypeError:...
============================== 2 failed in 0.05s ===============================

As you can see, passing a fixture as argument in a parameterized test doesn't work.

Solution

To make that possible, we have two alternatives. The first one is using request.getfixturevalue, which is available on pytest. This function dynamically runs a named fixture function.

@pytest.mark.parametrize(
    "a, b",
    [
        ("one", "four"),
        ("two", "three"),
    ],
)
def test_multiply_is_even_request(a, b, request):
    """Assert that an odd number times even is even."""
    a = request.getfixturevalue(a)
    b = request.getfixturevalue(b)
    assert is_even(a * b)

If we run the test again we get the following:

============================= test session starts ==============================
...
collecting ... collected 2 items

tests/test_variables.py::test_multiply_is_even_request[one-four] PASSED  [ 50%]
tests/test_variables.py::test_multiply_is_even_request[two-three] PASSED [100%]

============================== 2 passed in 0.02s ===============================

Process finished with exit code 0

Great! It works like a charm. However, there’s one more alternative, and for that we’ll need a third-party package called pytest-lazy-fixture. Let’s see how the test looks like using this lib.

@pytest.mark.parametrize(
    "a, b",
    [
        (pytest.lazy_fixture(("one", "four"))),
        # same as (pytest.lazy_fixture(("two", "three")))
        (pytest.lazy_fixture("two"), pytest.lazy_fixture("three")), 
    ],
)
def test_multiply(a, b):
    """Assert that an odd number times even is even."""
    assert is_even(a * b)

In this example, we use it by passing a tuple with the fixtures names or passing each one of them as a different argument. When we run this test, we can see it passes!

============================= test session starts ==============================
...
collecting ... collected 2 items

tests/test_variables.py::test_multiply[one-four] PASSED                  [ 50%]
tests/test_variables.py::test_multiply[two-three] PASSED                 [100%]

============================== 2 passed in 0.02s ===============================

Process finished with exit code 0

Conclusion

That’s it for today, folks! I hope you’ve learned something different and useful. Being able to reuse fixtures in parametrized tests is a must when we want to avoid repetition. Unfortunately, pytest doesn’t support that yet. On the other hand, we can make it happen either by using getfixturevalue in pytest or through a third-party library.

Other posts you may like:

See you next time!

This post was originally published at https://miguendes.me