When to use pytest fixtures?
Asked Answered
T

6

50

I'm new to testing and I've stumbled across pytest fixtures, but I'm not entirely sure when to use them and why they're useful.

For example, see the below code:

import pytest

@pytest.fixture
def input_value():
   input = 39
   return input

def test_divisible_by_3(input_value):
   assert input_value % 3 == 0

def test_divisible_by_6(input_value):
   assert input_value % 6 == 0

What is the function of the pytest.fixture here? Why can't we simply create a function called input_value() and run that function inside of the test function? For example:

import pytest

def input_value():
   input = 39
   return input

def test_divisible_by_3():
   assert input_value() % 3 == 0

def test_divisible_by_6():
   assert input_value() % 6 == 0

Why can't we just do this? What's the use of using fixtures?

Triacid answered 14/6, 2020 at 17:55 Comment(3)
pytest fixtures are a generalized way to set up/tear down a test case or a test suite, your example it indeed doesn't make much of a difference between a fixture and a regular function. Did you read through pytest fixtures: explicit, modular, scalable? This should give you an overview of what is possible to do with fixtures.Helsie
I can only agree with the question: pytest fixtures seem to be a re-invention of existing language features, actually introducing redundancy and complexity, making things harder rather than easier. Python itself is explicit, modular and scalable already, so why not just use these features also for test fixtures?Hite
I'd go even further than OP: Why do we need functions at all, whether they're fixtures or regular functions, if all they're doing is returning a static value? In OP's example above, wouldn't it be cleaner to get rid of input_value() altogether and just define a global variable (eg. INPUT = 39) that you can then use as input to your test functions like any other argument (eg. def test_divisible_by_3(input_value=INPUT):)?Mocha
V
29

Both Pytest Fixtures and regular functions can be used to structure to test code and reduce code duplication. The answer provided by George Udosen does an excellent job explaining that.

However, the OP specifically asked about the differences between a pytest.fixture and a regular Python function and there is a number of differences:

Pytest Fixtures are scoped

By default, a pytest.fixture is executed for each test function referencing the fixture. In some cases, though, the fixture setup may be computationally expensive or time consuming, such as initializing a database. For that purpose, a pytest.fixture can be configured with a larger scope. This allows the pytest.fixture to be reused across tests in a module (module scope) or even across all tests of a pytest run (session scope). The following example uses a module-scoped fixture to speed up the tests:

from time import sleep
import pytest


@pytest.fixture(scope="module")
def expensive_setup():
    return sleep(10)

def test_a(expensive_setup):
    pass  # expensive_setup is instantiated for this test

def test_b(expensive_setup):
    pass  # Reuses expensive_setup, no need to wait 10s

Although different scoping can be achieved with regular function calls, scoped fixtures are much more pleasant to use.

Pytest Fixtures are based on dependency injection

Pytest registers all fixtures during the test collection phase. When a test function requires an argument whose name matches a registered fixture name, Pytest will take care that the fixture is instantiated for the test and provide the instance to the test function. This is a form of dependency injection.

The advantage over regular functions is that you can refer to any pytest.fixture by name without having to explicitly import it. For example, Pytest comes with a tmp_path fixture that can be used by any test to work with a temporary file. The following example is taken from the Pytest documentation:

CONTENT = "content"


def test_create_file(tmp_path):
    d = tmp_path / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text(CONTENT)
    assert p.read_text() == CONTENT
    assert len(list(tmp_path.iterdir())) == 1
    assert 0

The fact that users don't have to import tmp_path before using it is very convenient.

It is even possible to apply a fixture to a test function without the test function requesting it (see Autouse fixtures).

Pytest fixtures can be parametrized

Much like test parametrization, fixture parametrization allows the user to specify multiple "variants" of a fixture, each with a different return value. Every test using that fixture will be executed multiple times, once for each variant. Say you want to test that all your code is tested for HTTP as well as HTTPS URLs, you might do something like this:

import pytest


@pytest.fixture(params=["http", "https"])
def url_scheme(request):
    return request.param


def test_get_call_succeeds(url_scheme):
    # Make some assertions
    assert True

The parametrized fixture will cause each referencing test to be executed with each version of the fixture:

$ pytest
tests/test_fixture_param.py::test_get_call_succeeds[http] PASSED                                                                                                                                                                         [ 50%]
tests/test_fixture_param.py::test_get_call_succeeds[https] PASSED                                                                                                                                                                        [100%]

======== 2 passed in 0.01s ========

Conclusion

Pytest fixtures provide many quality-of-life improvements over regular function calls. I recommend to always prefer Pytest fixtures over regular functions, unless you must be able to call the fixture directly. Directly invoking pytest fixtures is not indended and the call will fail.

Viafore answered 21/5, 2021 at 11:44 Comment(5)
I don't see the quality-of-life improvements in fixtures, just the additional effort needed to get them to work. For example, referring to a fixture without having to import it is worse, code is organised into modules for a reason; this also defeats autocompletion and type checking. Parametrisation can be done by passing the test's arguments to a regular function. Scope can be achieved with classes or module-global variables. Is there a drawback of regular functions that I'm missing?Amylolysis
You can achieve the same functionality without pytest. Also you don't have to make use of all features, like autouse fixtures. If it works better for you to use regular functions: go for it It's worth mentioning, though, that you might end up building your own testing framework. Developers new to your project will have to spend time learning your framework, whereas pytest is widely known already [1][2]. pytest also has other cool features besides fixtures :) [1] jetbrains.com/lp/python-developers-survey-2019 [2] jetbrains.com/lp/python-developers-survey-2020Viafore
"The advantage over regular functions is that you can refer to any pytest.fixture by name without having to explicitly import it". When looking at other peoples code it makes tracking down where a fixture is defined harder.Anxious
> "This allows the pytest.fixture to be reused across tests in a module (module scope) " This was already supported before fixtures: to get a module scope you put a setup() and teardown() at the top of the file; to get a session scope you put them in the __init__.py. It was far clearer.Inobservance
"initializing a database", using a persistent test database across different tests is a recipe for disasterPricillaprick
S
5

New to pytest myself but I know it reduces the need to write code that is used by multiple tests multiple times as in this your case you would have needed to rewrite that function severally, and this snippet would be a starter:

Fixtures are used to feed some data to the tests such as database connections, URLs to test and some sort of input data.

Therefore, instead of running the same code for every test, we can attach fixture function to the tests and it will run and return the data to the test before executing each test.

-- Source: https://www.tutorialspoint.com/pytest/pytest_fixtures.htm

Generally it helps to share resources among your tests that are common to them and greatly reduces duplication. Again the return values from these fixture functions can be passed as 'input parameters' into the individual tests as seen here:

@pytest.fixture
def input_value():
   input = 39
   return input

def test_divisible_by_3(input_value):
   assert input_value % 3 == 0
Secundas answered 14/6, 2020 at 18:4 Comment(7)
we can also write fixtures once in a separate module and reuse them everywhere without importing which is a great thing because we are separating arguments and their creation from test scenarios and tested APIBenzoate
What do you mean by I'd need to rewrite that function several times? I'd only need to write it once and then call it within the test functions?Triacid
I that is the point I am trying to make you don't need to do so with fixtures!Secundas
But why use a pytest fixture? Why can’t we just define a normal function with the same return as the pytest fixture?Triacid
Same question as @theman, why? Without @pytest.fixture, a normal function can do, and can be shared reused as well. I googled around but still little of them explaining why not using a normal function to share data inputs.Argyres
Agreed, came here because of the exact same question. Sharing fixtures amongst tests is a proper reason, as you can share a function just as easy. I guess what pytest is doing, is to run the fixture, save the return value and just reuse that instead of having to rerun the function every time it is used.Hegyera
IMHO the fundamental difference is when the fixture is used to initialize some state (say, populating a table in a database) and then tear that down after the test is run. If the fixture is just computing something, then indeed it has no fundamental advantage over functions.Fong
K
5

A fixture is used:

  • to have a specific function to be used many times and to simplify a test.

  • to do something before stating and after finishing test with yield.

For example, there are test_get_username() and test_set_check_password() in Django as shown below:

import pytest

from django.contrib.auth.models import User

@pytest.mark.django_db
def test_get_username():
    user = User.objects.create_user("test-user")
    user.username = "John"
    assert user.get_username() == "John"

@pytest.mark.django_db
def test_set_password_and_check_password():
    user = User.objects.create_user("test-user")
    user.set_password("new-password")
    assert user.check_password("new-password") == True

Then, you can create and use user() with @pytest.fixture to use it many times and to simplify test_get_username() and test_set_password_and_check_password() as shown below:

import pytest

from django.contrib.auth.models import User

@pytest.fixture # Here
def user():
    user = User.objects.create_user("test-user")
    return user

@pytest.mark.django_db
def test_get_username(user): # <- user
    user.username = "John"
    assert user.get_username() == "John"

@pytest.mark.django_db
def test_set_password_and_check_password(user): # <- user
    user.set_password("new-password")
    assert user.check_password("new-password") == True

And, for example, there are fixture_1() with yield and test_1() as shown below:

import pytest

@pytest.fixture
def fixture_1():
    print('Before Test')
    yield 6
    print('After Test')

def test_1(fixture_1):
    print('Running Test')
    assert fixture_1 == 6

Then, this is the output below:

$ pytest -q -rP
.                               [100%]
=============== PASSES ===============
_______________ test_1 _______________
------- Captured stdout setup --------
Before Test
-------- Captured stdout call --------
Running Test
------ Captured stdout teardown ------
After Test
1 passed in 0.10s

In addition, you can run multiple fixtures for test_1() and test_2() as shown below. *My answer explains how to call a fixture from another fixture in Pytest and my answer explains how to pass parameters or arguments to a fixture in Pytest and my answer explains how to use a fixture with @pytest.mark.parametrize() and my answer explains how to use fixtures as arguments in @pytest.mark.parametrize():

import pytest

@pytest.fixture
def fixture_1():
    return "fixture_1"

@pytest.fixture
def fixture_2():
    return "fixture_2"

def test_1(fixture_1, fixture_2):
    print(fixture_1, fixture_2)
    assert True

def test_2(fixture_1, fixture_2):
    print(fixture_1, fixture_2)
    assert True

Then, this is the output below:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout call --------- 
fixture_1 fixture_2
_______________ test_2 ________________ 
-------- Captured stdout call --------- 
fixture_1 fixture_2
2 passed in 0.33s

And, you can use request.getfixturevalue() to run fixture_1() and fixture_2() in test_1() and test_2() as shown below:

import pytest

@pytest.fixture
def fixture_1():
    return "fixture_1"

@pytest.fixture
def fixture_2():
    return "fixture_2"

def test_1(request):
    print(request.getfixturevalue("fixture_1"))
    print(request.getfixturevalue("fixture_2"))
    assert True

def test_2(request):
    print(request.getfixturevalue("fixture_1"))
    print(request.getfixturevalue("fixture_2"))
    assert True

Then, this is the output below:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout call --------- 
fixture_1
fixture_2
_______________ test_2 ________________ 
-------- Captured stdout call --------- 
fixture_1
fixture_2
2 passed in 0.10s

And, you can use @pytest.fixture(autouse=True) to run fixtures for all the tests test_1() and test_2() without setting fixtures as the tests' parameters as shown below. *A test cannot use fixtures' return values without fixtures' parameters:

import pytest

@pytest.fixture(autouse=True)
def fixture_1():
    print("fixture_1")

@pytest.fixture(autouse=True)
def fixture_2():
    print("fixture_2")

def test_1():
    assert True

def test_2():
    assert True

Then, this is the output below:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
_______________ test_2 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
2 passed in 0.33s

And, you can use @pytest.mark.usefixtures for test_1() and test_2() to run multiple fixtures as shown below. *A test cannot use fixtures' return values without fixtures' parameters:

import pytest

@pytest.fixture
def fixture_1():
    print("fixture_1")

@pytest.fixture
def fixture_2():
    print("fixture_2")

@pytest.mark.usefixtures('fixture_1', 'fixture_2')
def test_1():
    assert True

@pytest.mark.usefixtures('fixture_1', 'fixture_2')
def test_2():
    assert True

Then, this is the output below:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_______________ test_1 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
_______________ test_2 ________________ 
-------- Captured stdout setup -------- 
fixture_1
fixture_2
2 passed in 0.33s

And, you can use @pytest.mark.usefixtures for test_1() and test_2() in Test class to run multiple fixtures as shown below. *A fixture can use request.cls.something to pass data to the tests in a class:

import pytest

@pytest.fixture
def fixture_1(request):
    request.cls.first_name = "John"

@pytest.fixture
def fixture_2(request):
    request.cls.last_name = "Smith"

@pytest.mark.usefixtures('fixture_1', 'fixture_2')
class Test:
    def test_1(self):
        print(self.first_name, self.last_name)
        assert True

    def test_2(self):
        print(self.first_name, self.last_name)
        assert True

Then, this is the output below:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_____________ Test.test_1 _____________ 
-------- Captured stdout call --------- 
John Smith
_____________ Test.test_2 _____________ 
-------- Captured stdout call --------- 
John Smith
2 passed in 0.32s

And, you can use @pytest.fixture(autouse=True) to run fixtures for all the tests test_1() and test_2() in Test class without setting fixtures as the tests' parameters as shown below. *A fixture can use request.cls.something to pass data to the tests in a class:

import pytest

@pytest.fixture
def fixture_1(request):
    request.cls.first_name = "John"

@pytest.fixture
def fixture_2(request):
    request.cls.last_name = "Smith"

@pytest.mark.usefixtures('fixture_1', 'fixture_2')
class Test:
    def test_1(self):
        print(self.first_name, self.last_name)
        assert True

    def test_2(self):
        print(self.first_name, self.last_name)
        assert True

Then, this is the output below:

$ pytest -q -rP
..                               [100%]
=============== PASSES ================ 
_____________ Test.test_1 _____________ 
-------- Captured stdout call --------- 
John Smith
_____________ Test.test_2 _____________ 
-------- Captured stdout call --------- 
John Smith
2 passed in 0.33s
Karolynkaron answered 7/8, 2023 at 19:11 Comment(0)
F
2

As far as I can tell, the most important difference between functions and fixtures is that fixtures allow you to set up something for the test, possibly containing state, and then tear it down when the test is over.

The answers to the SO question What are fixtures in programming? are somewhat explicit about this.

If there's no state that needs to be set up and then torn down, there's no fundamental advantage to using a fixture, other than some syntactic sugar.

Fong answered 20/5, 2023 at 0:35 Comment(1)
I agree with this. Setup/teardown with yield is a really nice feature of pytest fixtures which regular Python functions would have trouble expressing neatly. The problem is that these days there are often higher-level mechanisms for keeping tests isolated, like rolling back transactions etc, so this kind of teardown is usually not needed any more.Radiosurgery
W
0

If you write a class with multiple methods where each method is a test, then you can write a setup and teardown method for this class. No fixture functionality needed.

However, if you write multiple files in a tree of folders with each file containing one or more test methods, then having a conftest.py file with setup() and teardown() fixtures which are run at the beginning and end of all tests in the folder tree, becomes very attractive.

Otherwise you would start to write a rudimentary test framework, make a parent class, init files for each folder in your tree, etc.

Wild answered 4/6 at 23:27 Comment(0)
S
0

Of course you can simply create a function called input_value() and run that function inside of the test function. Pytest fixtures intended for different purposes - mostly to define Setup/Teardown for test cases.

Sleave answered 16/6 at 10:30 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewToponym

© 2022 - 2024 — McMap. All rights reserved.