NewGuy
NewGuy

Reputation: 3423

How can I pass fixtures to pytest.mark.parameterize?

I am trying to pass three different fixtures to my pytest.mark.parameterize decorator, like this:

@pytest.mark.parametrize("credentials, return_code", [
    (user1, 200),
    #(user2, 200),
    #(user3, 401)
])
def test_login():
    assert True
# Actual test uses response.return_code == return_code

However, I get an error that reads:

NameError: name 'user1' is not defined

This isn't correct though, I have defined it in conftest.py:

user1_info = [('user1', 'password')]
@pytest.fixture(params=user1_info)
def user1(request):
    return request.param

The fixture works if I change my test to be like this:

def test_login(user1):
    assert True
# Actual test uses response.return_code == return_code

The problem is that if I do it this way, I have to write three separate tests, one for each fixture, and for each status_code check. If I could use parameterize, I could use one function and check the user/expected return code.

How can I utilize my fixtures are part of the pytest.mark.parameterize decorator?

Upvotes: 25

Views: 21024

Answers (4)

C.Nivs
C.Nivs

Reputation: 13106

There are two scenarios that you might encounter here:

1. I have a static fixture that I want to pass into a parameterized test

With this, you simply pass the fixture(s) in as the last argument(s) in your test function:

import pytest 

@pytest.fixture
def myfixture():
    return 'world'


@pytest.mark.parametrize('hello', ['Hello', 'HELLO'])
def test_world(hello, myfixture):
    output = hello.lower() + myfixture 
    assert output == 'helloworld'

2. I have a fixture that I want to match by name

Pass the request fixture and look up the one you want using getfixturevalue:

import pytest 

@pytest.fixture
def a():
    return 'a'

@pytest.fixture
def b():
    return 'b'


@pytest.mark.parametrize('fixture_name', ['a', 'b'])
def test_fixture_lookup(fixture_name, request):
    output = request.getfixturevalue(fixture_name)
    assert fixture_name == output

Upvotes: 1

Dunk
Dunk

Reputation: 1406

The pytest-cases library allows you to parametrize fixtures. A worked example:

import pytest_cases

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

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

@pytest_cases.parametrize("number", (one, two))
def test_it(number):
    assert number in [1, 2]

Upvotes: 1

JanKanis
JanKanis

Reputation: 6682

As far as I know, you can't. There are a few workarounds:

1: forego parametrization

def test_login():
    for user, return_code in [
      (User("user1", ...), 200),
      (User("user2", ...), 200),
      (User("user3", ...), 401),
    ]:
        # test user
        pass

or

def test_login(user1, user2, user3):
    for user, return_code in [
      (user1, 200),
      (user2, 200),
      (user3, 401),
    ]:
        # test user
        pass

Pro: simple
Con: If the first test fails, the later tests don't run. The error message also doesn't show which test case failed.

2: parametrize with an index or name

@pytest.mark.parametrize('idx', [0,1,2])
def test_login(idx):
    user, return_code = [
      (User("user1", ...), 200),
      (User("user2", ...), 200),
      (User("user3", ...), 401),
    ][idx]
        # test user
        pass

(or the equivalent using the user1, user2, user3 fixtures)

Pro: pytest recognizes the separate tests
Con: useless indexes/names

3: use an indirect parametrization fixture

@pytest.mark.fixture
def user_and_code(request):
    if request.param == "user1":
        return User("user1", ...), 200
    elif request.param == "user2":
        return User("user2", ...), 200
    elif request.param == "user3":
        return User("user3", ...), 401

@pytest.mark.parametrize('user_and_code', ['user1', 'user2', 'user3'], indirect=True)
def test_login(user_and_code):
    user, return_code = user_and_code

    # test user
    pass

Or

@pytest.mark.fixture
def user_and_code(request, user1, user2, user3):
    return [
        (user1, 200),
        (user2, 200),
        (user3, 401),
    ][request.param]

@pytest.mark.parametrize('user_and_code', [0,1,2], indirect=True)
def test_login(user_and_code):
    user, return_code = user_and_code

    # test user
    pass

Pro: test code is clean, fixture can be reused in other tests
Con: more complexity

According to the docs the indirect parametrization is meant to skip expensive setup during test collection, not so much to make fixtures available to parameters, which as you can see doesn't work very well.

There are a few choices that can be mixed and matched in each of the above options, e.g. instantiating all users or using if/else to only instantiate what you need, using the user1 etc fixtures or creating users inline, using names or indexes, etc. The examples only show some of the alternatives.

Upvotes: 4

damon
damon

Reputation: 15128

To pass fixtures as parametrized arguments, reference them as strings and use the keyword arguments indirect=['arg_name', ...].

Then use the parameter names credentials and return_code as arguments to your test function.

@pytest.mark.parametrize(
    "credentials, return_code",
    [
        ("user1", 200),
        ("user2", 200),
        ("user3", 401),
    ],
    # Only the `credentials` parameter is a reference to the
    # fixture. The `return_code` should be passed as is.
    indirect=["credentials"],
)
def test_login(credentials, return_code):
    return True

The documentation for pytest's parametrize magic with indirect can be found here: https://docs.pytest.org/en/stable/example/parametrize.html#apply-indirect-on-particular-arguments

Upvotes: 27

Related Questions