Search code examples
pytestfixtures

Pytest - is it possible to create a fixture that filters another fixture?


I have this code (Thanks, M.T):

url_list = [
    ['example1.com/x', 'example2.com/x'],
    ['example1.com/y', 'example2.com/y'],
    ['example1.com/z', 'example2.com/z'],
    ['example1.com/v', 'example2.com/v'],
    ['example1.com/w', 'example2.com/w'],
]

@pytest.fixture(scope="session", params=url_pairs)
def response_pairs(request):
    url_a, url_b = request.param
    yield requests.get(url_a), requests.get(url_b)

def test_1(response_pairs):
    response_a, response_b = response_pairs
    assert response_a.status_code == response_b.status_code
    except Exception as e:
        yield e, None

@pytest.mark.parametrize("field", ["one", "two", "three", "four", "five"])
def test_2(field, response_pairs):
    body_a = response_pairs[0].json()
    body_b = response_pairs[1].json()
    assert body_a[field] == body_b[field]

This works fine so far.

Now, I want test_2 to do different things depending on status_code.

So I created multiple copies of test_2 and added if statement to each one:

@pytest.mark.parametrize("field", ["one", "two", "three", "four", "five"])
def test_200(field, response_pairs):
    response_a, response_b = response_pairs
    if response_a.status_code == response_b.status_code == 200:
        body_a = response_a.json()
        body_b = response_b.json()
        assert body_a[field] == body_b[field]

@pytest.mark.parametrize("field", ["reason", "text"])
def test_404(field, response_pairs):
    response_a, response_b = response_pairs
    if response_a.status_code == response_b.status_code == 404:
        assert getattr(response_a, field, None).lower() == getattr(response_b, field, None).lower()

@pytest.mark.parametrize("field", ["reason", "text"])
def test_error(field, response_pairs):
    response_a, response_b = response_pairs
    if response_a.status_code == response_b.status_code and response_a.status_code not in [200, 404]:
        assert getattr(response_a, field, None).lower() == getattr(response_b, field, None).lower()

This works correctly, but I get all those "successful" tests for the False cases of those ifs

Is there any way to filter them out?

I tried created a fixture that filters out the successes:

@pytest.fixture
def responses_200(response_pairs):
    for response_a, response_b in response_pairs:
        if response_a.status_code == response_b.status_code == 200:
            yield response_a, response_b

(And same for 404s etc)

But that doesn't work. It doesn't get rid of meaningless tests, instead everything fails:

ValueError: too many values to unpack (expected 2)

So I tried removing response_pairs from parameter list:

@pytest.fixture
def responses_200(request):
    for response_a, response_b in response_pairs:
        if response_a.status_code == response_b.status_code == 200:
            yield response_a, response_b

And now setup fails with TypeError: 'function' object is not iterable:

TypeError: 'function' object is not iterable

I'm still learning how to use fixtures...

Is it possible to create a fixture that filters another fixture?


Solution

  • Without having reproduced your error, two remarks regarding the observed errrors:

    1. You need to add the name of the fixture you want to use to the parameter list (in this case response_pairs). If you remove it the value that gets returned from refering to its name in the function is not the fixture value you expect but the function itself (hence the error message that you are trying to iterate through a function)
    2. In any given tests I would expect response_pairs to only contain a single pair. Therefore I would try to remove the for loop and just unpack that single pair like so:
    @pytest.fixture
    def responses_200(response_pairs):
        response_a, response_b = response_pairs
        if response_a.status_code == response_b.status_code == 200:
            yield response_a, response_b
    

    I would expect this to avoid the observed errors but I'm not sure if it will produce the desired result of executing the tests only for status 200 responses.

    One slightly hacky way that you can at least avoid executing the tests is by skipping them from inside the fixture:

    Reduced example:

    import pytest
    
    rl_list = [
        ['example1.com/x', 200],
        ['example1.com/y', 404],
        ['example1.com/z', 200],
    ]
    
    @pytest.fixture(scope="session", params=rl_list)
    def url_response_pair(request):
        url, response = request.param
        yield url, response
    
    @pytest.fixture(scope="session")
    def response_200(url_response_pair):
        url, response = url_response_pair
        if response != 200:
            pytest.skip('Skipping 200 test for response code {response} from url {url}')
    
    def test_something_for_response_200(response_200):
        print("testing something")