Filtering pytest fixtures

Viewed 764

This is basically the same as an older question but hoping there is a better solution now.

The problem: Given a parametrized fixture, how can one parametrize a test function with a subset of the fixtures objects?

Example:

import pytest

@pytest.fixture(params=range(7))
def square(request):
    return request.param ** 2

def test_all_squares(square):
    sqrt = square ** 0.5
    assert sqrt == int(sqrt)

@pytest.fixture()
def odd_square(square):
    if square % 2 == 1:
        return square
    pytest.skip()

def test_all_odd_squares(odd_square):
    assert odd_square % 2 == 1
    sqrt = odd_square ** 0.5
    assert sqrt == int(sqrt)

Output:

$ pytest pytests.py  -v
=================================================================== test session starts ===================================================================
...
collected 14 items                                                                                                                                        

pytests.py::test_all_squares[0] PASSED                                                                                                              [  7%]
pytests.py::test_all_squares[1] PASSED                                                                                                              [ 14%]
pytests.py::test_all_squares[2] PASSED                                                                                                              [ 21%]
pytests.py::test_all_squares[3] PASSED                                                                                                              [ 28%]
pytests.py::test_all_squares[4] PASSED                                                                                                              [ 35%]
pytests.py::test_all_squares[5] PASSED                                                                                                              [ 42%]
pytests.py::test_all_squares[6] PASSED                                                                                                              [ 50%]
pytests.py::test_all_odd_squares[0] SKIPPED                                                                                                         [ 57%]
pytests.py::test_all_odd_squares[1] PASSED                                                                                                          [ 64%]
pytests.py::test_all_odd_squares[2] SKIPPED                                                                                                         [ 71%]
pytests.py::test_all_odd_squares[3] PASSED                                                                                                          [ 78%]
pytests.py::test_all_odd_squares[4] SKIPPED                                                                                                         [ 85%]
pytests.py::test_all_odd_squares[5] PASSED                                                                                                          [ 92%]
pytests.py::test_all_odd_squares[6] SKIPPED                                                                                                         [100%]

============================================================== 10 passed, 4 skipped in 0.02s ==============================================================

Although this works, it's not ideal:

  • It creates dummy test cases that are always skipped
  • It requires test functions that use the filtered fixture to give a different name (odd_square) to the parameter.

What I'd like instead is e.g. a filter_fixture(predicate, fixture) function that filters the original fixture's objects and can be passed to pytest.mark.parametrize, something like this:

@pytest.mark.parametrize("square", filter_fixture(lambda x: x % 2 == 1, square))
def test_all_odd_squares(square):
    assert square % 2 == 1
    sqrt = square ** 0.5
    assert sqrt == int(sqrt)

Is this feasible somehow?

1 Answers

If you require a certain degree of logic to determine which parameters to apply to each test, you might want to consider using the pytest_generate_tests hook.

The hook function pytest_generate_tests is called for every collected test. The metafunc argument allows you to dynamically parametrize each individual test case. Rewriting your example to use pytest_generate_tests could look like this:

def pytest_generate_tests(metafunc):
    square_parameters = (x**2 for x in range(7))
    if 'square' in metafunc.fixturenames:
        metafunc.parametrize("square", square_parameters)
    if 'odd_square' in metafunc.fixturenames:
        odd_square_parameters = (x for x in square_parameters if x % 2 == 1)
        metafunc.parametrize("odd_square", odd_square_parameters)

def test_all_squares(square):
    sqrt = square ** 0.5
    assert sqrt == int(sqrt)

def test_all_odd_squares(odd_square):
    assert odd_square % 2 == 1
    sqrt = odd_square ** 0.5
    assert sqrt == int(sqrt)

This results in the following test cases being run:

$ pytest -v pytests.py 
=========== test session starts ===========
…
collected 10 items

pytests.py::test_all_squares[0] PASSED                                                                                                                                                                                     [ 10%]
pytests.py::test_all_squares[1] PASSED                                                                                                                                                                                     [ 20%]
pytests.py::test_all_squares[4] PASSED                                                                                                                                                                                     [ 30%]
pytests.py::test_all_squares[9] PASSED                                                                                                                                                                                     [ 40%]
pytests.py::test_all_squares[16] PASSED                                                                                                                                                                                    [ 50%]
pytests.py::test_all_squares[25] PASSED                                                                                                                                                                                    [ 60%]
pytests.py::test_all_squares[36] PASSED                                                                                                                                                                                    [ 70%]
pytests.py::test_all_odd_squares[1] PASSED                                                                                                                                                                                 [ 80%]
pytests.py::test_all_odd_squares[9] PASSED                                                                                                                                                                                 [ 90%]
pytests.py::test_all_odd_squares[25] PASSED                                                                                                                                                                                [100%]

=========== 10 passed in 0.03s ===========

Note that the test IDs in my example are slightly different from yours. However, you can provide explicit test identifiers using the ìds argument of metafunc.parametrize.

Related