How to pass a parameter to a fixture function in Pytest?
Asked Answered
G

14

265

I am using py.test to test some DLL code wrapped in a python class MyTester. For validating purpose I need to log some test data during the tests and do more processing afterwards. As I have many test_... files I want to reuse the tester object creation (instance of MyTester) for most of my tests.

As the tester object is the one which got the references to the DLL's variables and functions I need to pass a list of the DLL's variables to the tester object for each of the test files (variables to be logged are the same for a test_... file). The content of the list is used to log the specified data.

My idea is to do it somehow like this:

import pytest

class MyTester():
    def __init__(self, arg = ["var0", "var1"]):
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

# located in conftest.py (because other test will reuse it)

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester()
    return _tester

# located in test_...py

# @pytest.mark.usefixtures("tester") 
class TestIt():

    # def __init__(self):
    #     self.args_for_tester = ["var1", "var2"]
    #     # how to pass this list to the tester fixture?

    def test_tc1(self, tester):
       tester.dothis()
       assert 0 # for demo purpose

    def test_tc2(self, tester):
       tester.dothat()
       assert 0 # for demo purpose

Is it possible to achieve it like this or is there even a more elegant way?

Usually I could do it for each test method with some kind of setup function (xUnit-style). But I want to gain some kind of reuse. Does anyone know if this is possible with fixtures at all?

I know I can do something like this: (from the docs)

@pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"])

But I need to the parametrization directly in the test module. Is it possible to access the params attribute of the fixture from the test module?

Gluey answered 2/8, 2013 at 8:11 Comment(0)
B
318

This is actually supported natively in py.test via indirect parametrization.

In your case, you would have:

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [['var1', 'var2']], indirect=True)
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
Broccoli answered 23/11, 2015 at 19:34 Comment(9)
Ah, this is pretty nice (I think your example may be a little outdated though--it differs from the examples in the official docs). Is this a relatively new feature? I've never encountered it before. This is a good solution to the problem as well--in some ways better than my answer.Curler
I tried using this solution but was having issues passing multiple parameters or using variable names other than request. I ended up using @Curler 's solution.Ashworth
This should be the accepted answer. The official documentation for the indirect keyword argument is admittedly sparse and unfriendly, which probably accounts for the obscurity of this essential technique. I've scoured the py.test site on multiple occasions for this very feature – only to come up empty, older, and befuddled. Bitterness is a place known as continuous integration. Thank Odin for Stackoverflow.Jalapa
Note this method changes the name of your tests to include the parameter, which may or may not be desired. test_tc1 becomes test_tc1[tester0].Crispas
So indirect=True hands over parameters to all the called fixtures, right? Because the documentation explicitly names the fixtures for indirect parametrization, e.g. for a fixture named x: indirect=['x']Despumate
Okay, so True and False also works for the indirect keyword according to the official documentation about parametrization.Despumate
You can also use "parameter fixtures" instead of the indirect keyword. See https://mcmap.net/q/108854/-how-to-pass-a-parameter-to-a-fixture-function-in-pytestAdenovirus
indirect=['fixture1', 'bob_the_fixture'] also works for tests where specific named fixtures are 'parametrized' and is required for the case where other params are in play but do not have fixture namesakes.Borg
is it possible to pass down the parameter to another fixture requested by the current fixture?Holster
C
197

Update: Since this the accepted answer to this question and still gets upvoted sometimes, I should add an update. Although my original answer (below) was the only way to do this in older versions of pytest as others have noted pytest now supports indirect parametrization of fixtures. For example you can do something like this (via @imiric):

# test_parameterized_fixture.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [True, False], indirect=['tester'])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture.py::TestIt::test_tc1[True] PASSED                                                                                                                    [ 50%]
test_parameterized_fixture.py::TestIt::test_tc1[False] FAILED

However, although this form of indirect parametrization is explicit, as @Yukihiko Shinoda points out it now supports a form of implicit indirect parametrization (though I couldn't find any obvious reference to this in the official docs):

# test_parameterized_fixture2.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [True, False])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture2.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture2.py::TestIt::test_tc1[True] PASSED                                                                                                                   [ 50%]
test_parameterized_fixture2.py::TestIt::test_tc1[False] FAILED

I don't know exactly what are the semantics of this form, but it seems that pytest.mark.parametrize recognizes that although the test_tc1 method does not take an argument named tester_arg, the tester fixture that it's using does, so it passes the parametrized argument on through the tester fixture.


I had a similar problem--I have a fixture called test_package, and I later wanted to be able to pass an optional argument to that fixture when running it in specific tests. For example:

@pytest.fixture()
def test_package(request, version='1.0'):
    ...
    request.addfinalizer(fin)
    ...
    return package

(It doesn't matter for these purposes what the fixture does or what type of object the returned package) is.

It would then be desirable to somehow use this fixture in a test function in such a way that I can also specify the version argument to that fixture to use with that test. This is currently not possible, though might make a nice feature.

In the meantime it was easy enough to make my fixture simply return a function that does all the work the fixture previously did, but allows me to specify the version argument:

@pytest.fixture()
def test_package(request):
    def make_test_package(version='1.0'):
        ...
        request.addfinalizer(fin)
        ...
        return test_package

    return make_test_package

Now I can use this in my test function like:

def test_install_package(test_package):
    package = test_package(version='1.1')
    ...
    assert ...

and so on.

The OP's attempted solution was headed in the right direction, and as @hpk42's answer suggests, the MyTester.__init__ could just store off a reference to the request like:

class MyTester(object):
    def __init__(self, request, arg=["var0", "var1"]):
        self.request = request
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

Then use this to implement the fixture like:

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester(request)
    return _tester

If desired the MyTester class could be restructured a bit so that its .args attribute can be updated after it has been created, to tweak the behavior for individual tests.

Curler answered 17/2, 2015 at 20:36 Comment(6)
Thanks for the hint with the function inside the fixture. Did take some time until i could work on this again but this is pretty useful!Gluey
A nice short post on this topic: alysivji.github.io/pytest-fixures-with-function-arguments.htmlGluey
do you not getting an error saying: "Fixtures are not meant to be called directly, but are created automatically when test functions request them as parameters. "?Barrelhouse
Don't forget if you use scope to select @pytest.fixture(scope=function).Dissimulation
I don't get it. If you're going to call a function anyway, why not just do it plain and simple instead of wrapping it in a fixture? I thought the whole point of fixtures is that they magically get injected without asking.Loveinidleness
These are trivial examples but the point is not just "calling a function". The fixture in question might be something more complicated and expensive to set up.Curler
P
47

As @chilicheech pointed, this way is documented by official at least since pytest 6.2 (released at 2020-12-13):

It seems to work in latest version of pytest.

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [['var1', 'var2']])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
Prosperity answered 10/2, 2020 at 10:59 Comment(6)
Thanks for pointing this out--this seems like the cleanest solution of all. I don't think this used to be possible in earlier versions, but it's clear that it now is. Do you know if this form is mentioned anywhere in the official docs? I couldn't find anything quite like it, but it clearly works. I've updated my answer to include this example, thanks.Curler
I think it will not be possible in the feature, if you take a look at github.com/pytest-dev/pytest/issues/5712 and the related (merged) PR.Gaily
This was reverted github.com/pytest-dev/pytest/pull/6914Vingtetun
To clarify, @Vingtetun is indicating that the PR linked by Nadège was reverted. Thus, this undocumented feature (I think it's still undocumented?) still lives.Incubus
This is documented here: docs.pytest.org/en/7.2.x/how-to/…Slum
This doesn't seem to work if the scope of tester is not function. It appears that pytest autogenerates a fixture tester_arg, but with default function scope. So there is a scope mismatch if tester has any other scope. The problem does not exist with indirect parametrization.Stood
T
23

You can also use closures, which will give you more comprehensive naming and control on the params. They're more "explicit" than the request param used in the implicit parametrization:

@pytest.fixture
def tester():
    # Create a closure on the Tester object
    def _tester(first_param, second_param):
        # use the above params to mock and instantiate things
        return MyTester(first_param, second_param)
    
    # Pass this closure to the test
    yield _tester 


@pytest.mark.parametrize(['param_one', 'param_two'], [(1,2), (1000,2000)])
def test_tc1(tester, param_one, param_two):
    # run the closure now with the desired params
    my_tester = tester(param_one, param_two)
    # assert code here

I use this to build configurable fixtures.

Thermochemistry answered 7/7, 2021 at 13:1 Comment(2)
How do you do teardown for this method?Vinegarette
The fixture pattern is an abstraction on top of the setup/teardown traditional approach. You "set up" what you need before "yielding" the prepared object. then after yielding you can tear it down (close resources, remove files) if needed. If you need access to the instance I suppose you can save it from the outer scopeThermochemistry
A
14

To improve a little bit imiric's answer: Another elegant way to solve this problem is to create "parameter fixtures". I personally prefer it over the indirect feature of pytest. This feature is available from pytest_cases, and the original idea was suggested by Sup3rGeo.

import pytest
from pytest_cases import param_fixture

# create a single parameter fixture
var = param_fixture("var", [['var1', 'var2']], ids=str)

@pytest.fixture
def tester(var):
    """Create tester object"""
    return MyTester(var)

class TestIt:
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

Note that pytest-cases also provides @fixture that allow you to use parametrization marks directly on your fixtures instead of having to use @pytest.fixture(params=...)

from pytest_cases import fixture, parametrize

@fixture
@parametrize("var", [['var1', 'var2']], ids=str)
def tester(var):
    """Create tester object"""
    return MyTester(var)

and @parametrize_with_cases that allows you to source your parameters from "case functions" that may be grouped in a class or even a separate module. See doc for details. I'm the author by the way ;)

Adenovirus answered 28/3, 2019 at 9:30 Comment(2)
This appears to work now in plain pytest as well (I have v5.3.1). That is, I was able to get this working without param_fixture. See this answer. I couldn't find any example like it in the docs though; do you know anything about this?Curler
thanks for the info and the link ! I had no idea this was feasible. Let's wait for an official documentation to see what they have in mind.Adenovirus
P
12

You can access the requesting module/class/function from fixture functions (and thus from your Tester class), see interacting with requesting test context from a fixture function. So you could declare some parameters on a class or module and the tester fixture can pick it up.

Polacca answered 7/8, 2013 at 8:44 Comment(2)
I know i can do something like this: (from the docs) @pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"]) But i need to do it in the test module. How can i dynamically add params to the fixtures?Gluey
The point is not to have to interact with requesting test context from a fixture function but to have a well defined way to pass arguments to a fixture function. Fixture function shouldn't have to be aware of a type of requesting test context just to be able to receive arguments with agreed upon names. For instance one would like to be able to write @fixture def my_fixture(request) and then @pass_args(arg1=..., arg2=...) def test(my_fixture) and get these args in my_fixture() like this arg1 = request.arg1, arg2 = request.arg2. Is something like this possible in py.test now?Larrylars
A
12

I made a funny decorator that allows writing fixtures like this:

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"

Here, to the left of / you have other fixtures, and to the right you have parameters that are supplied using:

@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"

This works the same way function arguments work. If you don't supply the age argument, the default one, 69, is used instead. if you don't supply name, or omit the dog.arguments decorator, you get the regular TypeError: dog() missing 1 required positional argument: 'name'. If you have another fixture that takes argument name, it doesn't conflict with this one.

Async fixtures are also supported.

Additionally, this gives you a nice setup plan:

$ pytest test_dogs_and_owners.py --setup-plan

SETUP    F dog['Buddy', age=7]
...
SETUP    F dog['Champion']
SETUP    F owner (fixtures used: dog)['John Travolta']

A full example:

from plugin import fixture_taking_arguments

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"


@fixture_taking_arguments
def owner(request, dog, /, name="John Doe"):
    yield f"{name}, owner of {dog}"


@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"


@dog.arguments("Champion")
class TestChampion:
    def test_with_dog(self, dog):
        assert dog == "Champion the dog aged 69"

    def test_with_default_owner(self, owner, dog):
        assert owner == "John Doe, owner of Champion the dog aged 69"
        assert dog == "Champion the dog aged 69"

    @owner.arguments("John Travolta")
    def test_with_named_owner(self, owner):
        assert owner == "John Travolta, owner of Champion the dog aged 69"

The code for the decorator:

import pytest
from dataclasses import dataclass
from functools import wraps
from inspect import signature, Parameter, isgeneratorfunction, iscoroutinefunction, isasyncgenfunction
from itertools import zip_longest, chain


_NOTHING = object()


def _omittable_parentheses_decorator(decorator):
    @wraps(decorator)
    def wrapper(*args, **kwargs):
        if not kwargs and len(args) == 1 and callable(args[0]):
            return decorator()(args[0])
        else:
            return decorator(*args, **kwargs)
    return wrapper


@dataclass
class _ArgsKwargs:
    args: ...
    kwargs: ...

    def __repr__(self):
        return ", ".join(chain(
               (repr(v) for v in self.args), 
               (f"{k}={v!r}" for k, v in self.kwargs.items())))


def _flatten_arguments(sig, args, kwargs):
    assert len(sig.parameters) == len(args) + len(kwargs)
    for name, arg in zip_longest(sig.parameters, args, fillvalue=_NOTHING):
        yield arg if arg is not _NOTHING else kwargs[name]


def _get_actual_args_kwargs(sig, args, kwargs):
    request = kwargs["request"]
    try:
        request_args, request_kwargs = request.param.args, request.param.kwargs
    except AttributeError:
        request_args, request_kwargs = (), {}
    return tuple(_flatten_arguments(sig, args, kwargs)) + request_args, request_kwargs


@_omittable_parentheses_decorator
def fixture_taking_arguments(*pytest_fixture_args, **pytest_fixture_kwargs):
    def decorator(func):
        original_signature = signature(func)

        def new_parameters():
            for param in original_signature.parameters.values():
                if param.kind == Parameter.POSITIONAL_ONLY:
                    yield param.replace(kind=Parameter.POSITIONAL_OR_KEYWORD)

        new_signature = original_signature.replace(parameters=list(new_parameters()))

        if "request" not in new_signature.parameters:
            raise AttributeError("Target function must have positional-only argument `request`")

        is_async_generator = isasyncgenfunction(func)
        is_async = is_async_generator or iscoroutinefunction(func)
        is_generator = isgeneratorfunction(func)

        if is_async:
            @wraps(func)
            async def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_async_generator:
                    async for result in func(*args, **kwargs):
                        yield result
                else:
                    yield await func(*args, **kwargs)
        else:
            @wraps(func)
            def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_generator:
                    yield from func(*args, **kwargs)
                else:
                    yield func(*args, **kwargs)

        wrapper.__signature__ = new_signature
        fixture = pytest.fixture(*pytest_fixture_args, **pytest_fixture_kwargs)(wrapper)
        fixture_name = pytest_fixture_kwargs.get("name", fixture.__name__)

        def parametrizer(*args, **kwargs):
            return pytest.mark.parametrize(fixture_name, [_ArgsKwargs(args, kwargs)], indirect=True)

        fixture.arguments = parametrizer

        return fixture
    return decorator
Aspergillosis answered 10/6, 2020 at 23:14 Comment(2)
I looks really pytest-like! Do you plan to submit this to upstream (into pytest)?Honey
@GeorgeShuklin well I went ahead and opened an issue for this, along with more crazy ideas github.com/pytest-dev/pytest/issues/8109Aspergillosis
F
6

Another way to do this is to use the request object to access variables defined in the module or class the test function is defined in.

This way you don't have to reuse the @pytest.mark.parametrize() decorator on every function of your test class if you want to pass the same variable for all the test functions of the class/module.

Example with a class variable :

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.cls.tester_args)


class TestIt:
    tester_args = ['var1', 'var2']

    def test_tc1(self, tester):
       tester.dothis()

    def test_tc2(self, tester):
       tester.dothat()

This way the tester object of both test_tc1 and test_tc2 will be initialized with the tester_args parameters.

You can also use:

  • request.function to access the test_tc1 function,
  • request.instance to access the TestIt class instance,
  • request.module to access the module TestIt is defined in
  • etc. (refer to the request documentation)
Ferromagnetic answered 9/10, 2020 at 13:48 Comment(0)
D
5

Another way to go with this is to use a custom mark. It looks better than parameterize in the code, is not reflected in the test name, and is also optional (can be defined as not optional by raising a failure if no such mark exists)

for example:

@pytest.fixture
def loaded_dll(request):
    dll_file = None
    for mark in request.node.iter_markers("dll_file"):
        if mark.args:
            if dll_file is not None:
                pytest.fail("Only one dll_file can be mentioned in marks")
            dll_file = mark.args[0]
    if dll_file is None:
        pytest.fail("dll_file is a required mark")
    return some_dll_load(dll_file)

@pytest.mark.dll_file("this.dll")
def test_this_dll(loaded_dll):
    ...

I used this for my tests when I needed a fixture that mocks an ssh client, and wanted to test different possible outputs, I could decide the output for each test using the mark.

Notice that if it's for personal usage, the failsave mechanisms that fail the test are not required.

Doubleripper answered 25/10, 2021 at 10:21 Comment(0)
L
1

Fixture works like decorator. I think it's easier and clearly. You also can use it

In conftest.py

@pytest.fixture
def tester():
    def wrapper(arg):
        _tester = MyTester(arg)
        return _tester
    return wrapper

In test.py

class TestIt():

   def test_tc1(self, tester, arg):  # test function takes fixture and arg
   mock_tester = tester(arg)  # mock_tester just an instance of MyTester
   mock_tester.dothis()  # so you get instance with initialized arg
   assert 0 # for demo purpose
Landlord answered 3/5, 2023 at 0:9 Comment(2)
Could you provide MyTester class implementation? ThanksChestnut
@Chestnut class MyTester(object): def __init__(self, request, arg=["var0", "var1"]): self.request = request self.arg = arg # self.use_arg_to_init_logging_part() def dothis(self): print "this" def dothat(self): print "that"Landlord
F
1

Ori Markovitch answer surely is the best trick for my case. The only disadvantage about it is that a fixture will not be shown as a parameterized testcase but in most cases I think this is okay. In my case I want to have all the test parameters above the testcase which this trick allow me to. I just want to point out that using the fixture mark trick you can both pass args and kwargs and you can also specify multiple args split in different markings. There is also a few things you need to do in terms of error handling but for the example I made, I kept it simple.

I made a few examples here where it shows how you can utilise this:

import pytest
from _pytest.mark import Mark


def get_marker(request, name) -> Mark:
    markers = list(request.node.iter_markers(name))
    if len(markers) > 1:
        pytest.fail(f"Found multiple markers for {name}")
    return markers[0]


class FixMark:
    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2


@pytest.fixture
def var_test(request):
    mark1 = get_marker(request, "var_test_var1")
    mark2 = get_marker(request, "var_test_var2")
    var1 = mark1.kwargs["var1"] if len(mark1.kwargs) else mark1.args[0]
    var2 = mark2.kwargs["var2"] if len(mark2.kwargs) else mark2.args[0]
    yield FixMark(var1, var2)


@pytest.fixture
def var_combined_test(request):
    mark = get_marker(request, "var_combined")
    var1 = mark.kwargs["var1"] if mark.kwargs.get("var1") else mark.args[0]
    var2 = mark.kwargs["var2"] if mark.kwargs.get("var2") else mark.args[1]
    yield FixMark(var1, var2)


@pytest.mark.var_test_var1("ABC")
@pytest.mark.var_test_var2("DEF")
def test_var_test_args(var_test):
    assert var_test.var1 == "ABC"
    assert var_test.var2 == "DEF"


@pytest.mark.var_test_var1(var1="ABC")
@pytest.mark.var_test_var2(var2="DEF")
def test_var_test_kwargs(var_test):
    assert var_test.var1 == "ABC"
    assert var_test.var2 == "DEF"


@pytest.mark.var_combined("ABC", "DEF")
def test_vars_combined_args(var_combined_test):
    assert var_combined_test.var1 == "ABC"
    assert var_combined_test.var2 == "DEF"


@pytest.mark.var_combined(var1="ABC", var2="DEF")
def test_vars_combined_kwargs(var_combined_test):
    assert var_combined_test.var1 == "ABC"
    assert var_combined_test.var2 == "DEF"
Fraught answered 17/12, 2023 at 13:50 Comment(1)
Great compact solution! But I have a problem with "Found multiple marker" when I use "@pytest.mark.parametrize". I have to disable it. Thanks!Autotrophic
W
0

For example, with @pytest.fixture(params=(...)), you can pass Apple, Orange and Banana to fruits() fixture itself, then to test() as shown below:

import pytest

@pytest.fixture(params=("Apple", "Orange", "Banana"))
def fruits(request):
    print("fruits", request.param) # 1st
    return request.param

def test(fruits):
    print("test", fruits) # 2nd
    assert True

Output:

$ pytest -q -rP
...                              [100%]
=============== PASSES ================ 
_____________ test[Apple] _____________ 
-------- Captured stdout setup -------- 
fruits Apple
-------- Captured stdout call --------- 
test Apple
____________ test[Orange] _____________ 
-------- Captured stdout setup -------- 
fruits Orange
-------- Captured stdout call --------- 
test Orange
____________ test[Banana] _____________ 
-------- Captured stdout setup -------- 
fruits Banana
-------- Captured stdout call --------- 
test Banana
3 passed in 0.11s

And for example, you can pass 2 arguments to addition() fixture which has the nested function core() as shown below:

import pytest

@pytest.fixture
def addition():
    def core(num1, num2):
        return num1 + num2
    return core

def test(request, addition):
    print(addition(2, 3))
    print(request.getfixturevalue("addition")(6, 8))
    assert True

Output:

$ pytest -q -rP
.                                [100%]
=============== PASSES ================ 
________________ test _________________ 
-------- Captured stdout call --------- 
5
14
1 passed in 0.10s
Wenwenceslaus answered 7/9, 2023 at 16:23 Comment(0)
T
0

In my case, I needed to delete a request by its id in the teardown of the fixture. The problem is that only during the test I could get my request id. So i needed a fixture that would take a parameter in the middle of the test and delete my request after the test. I've come up with something like this:

@pytest.fixture()
def delete_request():
    # setUp:
    local_request_id = None

    def get_request_id_from_test(request_id: int):
        nonlocal local_request_id
        local_request_id = request_id

    yield get_request_id_from_test

    # tearDown:
    api.delete_request(local_request_id)

def test_create_request(delete_request):
    # Array
    ...

    # Act
    ... # here i get request id after creating a request
    delete_request(request_id)
   
    ...

This decision don't require any marks and parametrisation

Tasteful answered 10/10, 2023 at 10:42 Comment(0)
M
0

Here's how you do this with named args in the fixture, complete with mypy typing support. Note the tuple value used in the parameterize decorator, which lets you separate args - add as many tuples into the param array as you like for parameterized test cases.

import os
from typing import Literal

import pytest


@pytest.fixture(autouse=False, scope="function")
def set_env(key: str, value: str):
    """Set an environment variable temporarily, backing up and restoring the original value if it was set before the test was run."""

    orig_value = os.getenv(key)

    os.environ[key] = value
    yield

    if orig_value is not None:
        os.environ[key] = orig_value
    else:
        del os.environ[key]


@pytest.mark.parametrize("key, value", [("TEST_ENV", "env_value")])
def test_get_env(set_env: Literal["TEST_ENV"] | Literal["env_value"]):
    assert os.getenv("TEST_ENV") == "env_value"

You can also append additional standard parameterized fixtures or variables – and pytest knows (magically?) to count the number of expected args in the fixture function, and pass the rest as standard args to the test:

@pytest.mark.parametrize(
    "key, value, default, expected, expected_default",
    [
        ("TEST_ENV", "env_value", None, "env_value", None),
        ("TEST_ENV", "env_value", "default_value", "env_value", "default_value"),
    ],
)
def test_get_env(
    set_env: None,
    default: str | None,
    expected: str,
    expected_default: str | None,
):
    assert os.getenv("TEST_ENV") == expected
    assert os.environ.get("TEST_ENV") == expected
    assert os.environ.get("NOT_SET_ENV", default) == expected_default
Misunderstanding answered 14/3, 2024 at 17:22 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.