Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When to use pytest fixtures?

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?

like image 433
the man Avatar asked Jun 14 '20 17:06

the man


2 Answers

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.

like image 62
Michael Seifert Avatar answered Nov 12 '22 08:11

Michael Seifert


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
like image 42
George Udosen Avatar answered Nov 12 '22 08:11

George Udosen