61

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?

pyzzled
  • 611
  • 1
  • 5
  • 4

4 Answers4

104

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
supamaze
  • 8,169
  • 2
  • 11
  • 11
  • 7
    I like this and it's very functional – Tommy Feb 26 '18 at 19:12
  • 3
    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? – Jacob Pavlock Aug 15 '20 at 11:47
  • 11
    @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 value – nightgaunt Aug 26 '20 at 05:50
  • This saved a lot of my time. Thanks! – Amogh Mishra Dec 31 '21 at 19:52
  • 1
    @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. – Amit Tendulkar Dec 29 '22 at 12:54
  • 2
    @AmitTendulkar -- use `return` in the inner function, then in the fixture `yield` the inner function, finally adding teardown code. – Alexandru Dinu Mar 13 '23 at 13:07
17

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://stackoverflow.com/a/33879151/3858507


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)
Valentin Lorentz
  • 9,556
  • 6
  • 47
  • 69
nitzpo
  • 497
  • 2
  • 6
8

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.

Alfe
  • 56,346
  • 20
  • 107
  • 159
6

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()