Optional argument in pytest.mark.parametrize

Viewed 602

I have a long test with pytest framework. The code is like this:

@pytest.fixture(scope='function')
def my_setup():
        execute_setup_actions()

@pytest.mark.parametrize('name, arg1', [
        ('test_1', 1),
        ...
        ('test_100', 100),
])
def test_mine(name, arg1):
         execute_test_case(arg1)

Now I need an optional argument arg2 for my_setup fixture my_setup(arg2=None) used in one new test written in @pytest.mark.parametrize. Certainly I can put 'name, arg1, arg2' in parametrize and add None argument values for other 100 tests, but is there any other method to do such a thing in a more pretty way?

Thanks!

3 Answers

The most standard (i.e. canonical) way of passing parameters into fixtures is to use pytest's indirect arguments.

You can either add this new argument to all parameters tuples, or if you wish to avoid it, you can create a new test method that is identical to the first method, but it would only contain the new tuples, like so:

import pytest


def execute_setup_actions(fixture_param):
    print(f"entered execute_setup_actions with {fixture_param}")


@pytest.fixture(scope='function')
def my_setup(request):
    fixture_param = getattr(request, 'param', None)  # or your default value
    execute_setup_actions(fixture_param)
    return fixture_param


def execute_test_case(arg1, arg2):
    print(f"entered execute_test_case with {arg1}, {arg2}")


@pytest.mark.parametrize('name, arg1', [
    ('test_1', 1),
    ('test_100', 100),
])
def test_mine(my_setup, name, arg1):
    execute_test_case(arg1, my_setup)


@pytest.mark.parametrize('my_setup, name, arg1', [
    (20, 'test_101', 1),
    (2020, 'test_102', 100),
], indirect=["my_setup"])
def test_mine_with_extra_parameter(my_setup, name, arg1):
    execute_test_case(arg1, my_setup)

If you want to pass an argument to a fixture, you should use the Factory as Fixture pattern, as explained in the documentation:

@pytest.fixture(scope='function')
def my_setup():
    def _my_setup(arg2=None)
        execute_setup_actions(arg2)

    return _my_setup

And then, on your test:

@pytest.mark.parametrize('name, arg1', [
        ('test_1', 1),
        ...
        ('test_100', 100),
])
def test_mine(name, arg1, my_setup):
    my_setup(arg2)
    execute_test_case(arg1)

Instead of adding arg2 to the parametrize marker as you suggested, you could just write another test, but, as you would have only one value for arg2, you wouldn't need to parametrize it:

@pytest.mark.parametrize('name, arg1', [
        ('test_1', 1),
        ...
        ('test_100', 100),
])
def test_mine1(name, arg1):
         execute_test_case(arg1)


def test_mine2(name):
         execute_test_case(arg1, arg2)

Please note that there is no value for the name fixture on the second test because it's not being used on the first one (more on that below).

Now, there are some inconsistencies with your code:

  1. The my_setup fixture is not being used. You should add the autouse=True option or add it to the test signature.
  2. You're not using the name parameter on your test.
  3. Would the name parameter be the test id? You could then use the id option on the parametrize marker (more here).

So, based on your needs and the issues I saw, I would refactor your tests like this:

names_list = ['test_1' ... 'test_100']
arg1_list = [1 ... 100]

@pytest.mark.parametrize('arg1', arg1_list, ids=names_list)
def test_mine1(arg1):
         execute_test_case(arg1)


def test_mine2():
         execute_test_case(arg1, arg2)

Here is a way to go about flexible table driven tests.

import pytest

def convert(input: str, options: dict) -> str:
    if options:
        return globals()[options](input)
    return lower(input)

def upper(input: str) -> str:
    return input.upper()

def lower(input: str) -> str:
    return input.lower()

class TestClass:
    tests = [
        {"input": "FOO", "expected": "foo"},
        {"input": "bar", "expected": "BAR", "options": "upper"},
        {"input": "BAZ", "expected": "baz", "options": "lower"},
    ]

    @pytest.mark.parametrize('tests', tests)
    def test(self, tests):
        actual = convert(tests["input"], tests.get("options"))
        assert actual == tests["expected"]
Related