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?
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:
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 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).
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 ========
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With