How to pass arguments to pytest fixtures?
Asked Answered
B

5

71

The baseline of all my tests is that there will always be a taxi with at least one passenger in it. I can easily achieve this setup with some basic fixtures:

from blah import Passenger, Taxi

@pytest.fixture
def passenger():
    return Passenger()

@pytest.fixture
def taxi(passenger):
    return Taxi(rear_seat=passenger)

Testing the baseline is straightforward:

def test_taxi_contains_passenger(taxi)
    assert taxi.has_passenger()

My issue crops up when I start needing more complicated test setup. There will be scenarios where I'll need the taxi to have more than one passenger and scenarios where I'll need to define passenger attributes. For example:

def test_three_passengers_in_taxi(taxi)
    assert taxi.has_passengers(3)
    assert taxi.front_passenger_is_not_a_child()

I'm able to get around this problem by having specific fixtures for specific tests. For the above test, I would create the following fixture:

@pytest.fixture
def three_passenger_test_setup(taxi)
    taxi.add_front_seat_passenger(Passenger(child=False))
    taxi.add_rear_seat_passenger(Passenger())
    return taxi

I can pass the above fixture into my test case and everything is dandy, but if I go down this route I might end up with a fixture for every test and it feels like there should be a more efficient way of doing this.

Is there a way to pass arguments to a fixture so that those arguments can be used in creating the object the fixture returns? Should I be parameterizing the test function? The fixture? Or am I wasting time and is a fixture per test the way to go?

Butte answered 21/6, 2017 at 13:22 Comment(0)
L
120

We can do this by using a method that takes args within a fixture and return the method from the fixture.

let me show you an example

@pytest.fixture
def my_fixture():

  def _method(a, b):
    return a*b

  return _method

def test_me(my_fixture):
  result1 = my_fixture(2, 3)
  assert result1 == 6

  result2 = my_fixture(4, 5)
  assert result2 == 20
Lamas answered 22/6, 2017 at 14:11 Comment(6)
I like this and it's very functionalUnderpart
Can you explain this a little more? Why can I pass arguments to my_fixture() if it doesn't take any arguments? Also, what happens if I am using more than one fixture in my test case, but I only want to pass arguments to one of them?Hospitalize
@JacobPavlock Think of it this way. The fixture function my_fixture gets called when you pass it as an argument to test_me. The returned value is stored in my_fixture argument. So when you call my_fixture within test_me you are actually calling the returned valueEmeric
This saved a lot of my time. Thanks!Atombomb
@supamaze, how will this work if I want my fixture to have setup and teardown parts? Putting yield in the inner function before teardown steps doesn't work.Jacklin
@AmitTendulkar -- use return in the inner function, then in the fixture yield the inner function, finally adding teardown code.Vallery
R
19

Is there a way to pass arguments to a fixture so that those arguments can be used in creating the object the fixture returns? Should I be parameterizing the test function?

You can use test parametrization with indirect=True. In the pytest docs: Apply indirect on particular arguments. As displayed here: https://mcmap.net/q/108854/-how-to-pass-a-parameter-to-a-fixture-function-in-pytest


The fixture?

Another option that might suit you is using some fixture that specifies the argument using parametrization:

@pytest.fixture(params=[3,4])
def number_of_passengers(request):
    return request.param

and then accessing this fixture from the taxi and the test itself:

@pytest.fixture
def taxi(number_of_passengers):
    return Taxi(rear_seat=Passenger() * number_of_passengers)

def test_three_passengers_in_taxi(taxi, number_of_passengers)
    assert taxi.has_passengers(number_of_passengers)
    assert taxi.front_passenger_is_not_a_child()

This way is good if your tests and asserts are very similar between the cases you have.


Or am I wasting time and is a fixture per test the way to go?

I'd say you definitely shouldn't create a fixture for every test function. For that, you can just put the setup inside the test. This is actually a viable alternative in the case that you have to make different asserts for different cases of the taxi.


And finally another possible pattern you can use is a taxi factory. While for the example you've presented its not quite useful, if multiple parameters are required and only some are changing you can create a fixture similar to the following:

from functools import partial
@pytest.fixture
def taxi_factory():
    return partial(Taxi, 1, 2, 3)
Redoubt answered 21/6, 2017 at 20:45 Comment(0)
T
11

TLDR; Use pytest.mark and the request fixture to access request.keywords

This is a very old question, but existing answers did not work for me, so here is my solution using pytest.mark

from blah import Passenger, Taxi

@pytest.fixture
def passenger():
    return Passenger()

@pytest.fixture
def taxi(passenger, request):
    if "taxi" in request.keywords:
        kwargs = request.keywords["taxi"].kwargs
    else:
        kwargs = dict(rear_seat=passenger)
    return Taxi(**kwargs)


# This allows testing the baseline as-is...

def test_taxi_contains_passenger(taxi)
    assert taxi.has_passenger()

# and also using pytest.mark to pass whatever kwargs:

@pytest.mark.taxi(rear_seat=[Passenger()] * 3)
def test_three_passengers_in_taxi(taxi)
    assert taxi.has_passengers(3)
    assert taxi.front_passenger_is_not_a_child()

Tripartite answered 11/10, 2022 at 17:22 Comment(2)
Awesome! This worked for me and it makes a brief and clear way to declare tests!Tyrrell
This feels like a much more intuitive approach than the accepted answer. Works nicely.Hide
D
9

That fixture is just a Python decorator.

@decorator
def function(args):
  ...

is fancy for

def function(args):
  ...
function = decorator(function)

So you just might be able to write your own decorator, wrapping up the function you want to decorate in whatever you need and the fixture:

def myFixture(parameter):
  def wrapper(function):
    def wrapped(*args, **kwargs):
      return function(parameter, *args, **kwargs)
    return wrapped
  return pytest.fixture(wrapper)

@myFixture('foo')
def function(parameter, ...):
  ...

This will act like the fixture but will pass a value ('foo') as parameter to function.

Delegate answered 21/6, 2017 at 13:40 Comment(0)
K
0

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
Karen answered 8/9, 2023 at 21:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.