Python How to use fixture in pytest to share a function

eye-catchPython
Sponsored links

If you want the complete code, you can go to my GitHub repository.

Sponsored links

What is fixture

Some test cases need the same pre-process or post-process but you don’t want to add the code to all the test cases. fixture makes the implementation easy. It runs those processes automatically with a minimum code.

You just need to add @pytest.fixture() to the function that is used by different unit tests. Then, the function name needs to be used as an arg in the test function.

@pytest.fixture()
def func1():
    return "This is func1"


def test_1_1():
    print("fixture is not used")
    pass


def test_1_2(func1): # specify the function name
    print(func1)
    pass

The result looks like this.

$ pytest src/test/test_pytest2.py -vs

src/test/test_pytest2.py::test_1_1
fixture is not used
PASSED

src/test/test_pytest2.py::test_1_2
This is func1
PASSED

test_1_1 doesn’t use fixture but test_1_2 uses it.

Sponsored links

Change the function name

fixture has several parameters. In a case where you give a long name to the function but want to use another name for brevity, you can set the name.

@pytest.fixture(name="func2_name")
def func2():
    print("func2 --- inner")
    return "This is func2"

# ERROR
def test_2_1(func2):
    pass


def test_2_2(func2_name):
    print(func2_name)
    pass

By passing the name to name parameter, you can use the name in the function parameter.
test_2_1 fails because func2 doesn’t exist.

$ pytest src/test/test_pytest2.py -vs

src/test/test_pytest2.py::test_2_1 
ERROR

src/test/test_pytest2.py::test_2_2
func2 --- inner
This is func2
PASSED

...

  def test_2_1(func2):
E       fixture 'func2' not found
>       available fixtures: before_after, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, fixture_class, fixture_function, fixture_module, fixture_session, func1, func2_name, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

You can specify the name only to the function that you need to run the process.

If you need to run the function for all test cases, you can set autouse=True to the fixture like this below.

@pytest.fixture(autouse=True)
def func_for_all():
    pass

This will be explained again afterward.

usefixtures if the function returns nothing or the value is not used

If the returned value is not used in the test case, it’s better to use usefixtures instead because specifying a non-used parameter looks not good.

@pytest.fixture(name="func2_name")
def func2():
    print("func2 --- inner")
    return "This is func2"

@pytest.mark.usefixtures("func2_name")
def test_2_3(): # <--- no unused parameter "func2_name"
    pass

# src/test/test_pytest2.py::test_2_3
# func2 --- inner
# PASSED

test_2_3() doesn’t have an unused parameter but func2() is executed when the test is executed.

Define pre-process and post-process

In the previous example, return keyword was used. The function is processed before a test actually runs.

If you need to define both pre/post processes, you can write in the following way with yield keyword.

@pytest.fixture(autouse=True)
def before_after():
    # Process before a test execution
    print("\nBEFORE ---")

    yield  # Actual test is executed here

    # Process after a test execution
    print("\nAFTER ---")

In this way, you can, for example, open a file here before yield keyword for pre-process, then, close the file after the test ends.

The result looks as follows

$ pytest src/test/test_pytest2.py -vs

src/test/test_pytest2.py::test_1_1
BEFORE ---
fixture is not used
PASSED
AFTER ---

src/test/test_pytest2.py::test_1_2
BEFORE ---
This is func1
PASSED
AFTER ---

src/test/test_pytest2.py::test_2_1 SKIPPED (Test fails)
src/test/test_pytest2.py::test_2_2
BEFORE ---
func2 --- inner
This is func2
PASSED
AFTER ---

src/test/test_pytest2.py::test_2_3
BEFORE ---
func2 --- inner
PASSED
AFTER ---

The pre/post processes are executed for all test cases because autouse=True is set.

Share the fixture by defining it in conftest.py

If you need to share the fixture, you should write the code in conftest.py. The functions defined in this file can be shared without doing anything.

I defined the following for the next chapter.

# conftest.py
import pytest

@pytest.fixture(scope='function')
def fixture_function():
    print('--- function')


@pytest.fixture(scope='class')
def fixture_class():
    print('--- class')


@pytest.fixture(scope='module')
def fixture_module():
    print('--- module')


@pytest.fixture(scope='session')
def fixture_session():
    print('--- session')

Specifying scope for the fixture

fixture has scope concept. If you use it properly, you can control how many functions are called in the test suite.

ScopeMeaning
functionExecuted for each test case (Default)
classExecuted once for a whole class
moduleExecuted once for a whole test file
sessionExecuted once for the test execution

The fixture with these scopes is defined in conftest.py, so we can use it directly in different files.

# test_pytest_scope.py
import pytest


class TestScope1:
    @pytest.mark.usefixtures(
        "fixture_function",
        "fixture_class",
        "fixture_module",
        "fixture_session")
    def test_one(self):
        pass

    @pytest.mark.usefixtures(
        "fixture_function",
        "fixture_class",
        "fixture_module",
        "fixture_session")
    def test_two(self):
        pass


# test_pytest_scope2.py
import pytest


class TestScope2:
    @pytest.mark.usefixtures(
        "fixture_function",
        "fixture_class",
        "fixture_module",
        "fixture_session")
    def test_one(self):
        pass

    @pytest.mark.usefixtures(
        "fixture_function",
        "fixture_class",
        "fixture_module",
        "fixture_session")
    def test_two(self):
        pass

The result looks as follows.

$ pytest src/test/test_pytest_scope*  -vs

src/test/test_pytest_scope.py::TestScope1::test_one --- session
--- module
--- class
--- function
PASSED
src/test/test_pytest_scope.py::TestScope1::test_two --- function
PASSED
src/test/test_pytest_scope2.py::TestScope2::test_one --- module
--- class
--- function
PASSED
src/test/test_pytest_scope2.py::TestScope2::test_two --- function
PASSED

The call count for each scope is the following.

ScopeCall count
session1
module2
class2
function4

Omitting import when and calling unstub

If we need to test both without/with stub for the global class/function, it needs to be cleaned up after each test. Look at the following test code.

import pytest

from pathlib import Path
from mockito import unstub, when


@pytest.fixture(autouse=True)
def unstub_after_test():
    yield
    unstub()


def test_1():
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "data123"


def test_2():
    when(Path).read_text().thenReturn("dummy data1")
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "dummy data1"


def test_3():
    when(Path).read_text().thenReturn("dummy data2")
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "dummy data2"


def test_4():
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "data123"

test_4 fails if unstub is not called after each test.

You might think you don’t want to write from mockito import unstub, when and a fixture to restore the stubbed function. Can we somehow omit them? pytest provides some fixtures by default. Available fixtures can be checked with --fixtures option.

$ pytest --fixtures
==================================================================================== test session starts ====================================================================================
platform linux -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: /workspaces/blogpost-python
collected 40 items                                                                                                                                                                          
cache -- .../_pytest/cacheprovider.py:510
    Return a cache object that can persist state between testing sessions.

capsys -- .../_pytest/capture.py:878
    Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.

capsysbinary -- .../_pytest/capture.py:906
    Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.

capfd -- .../_pytest/capture.py:934
    Enable text capturing of writes to file descriptors ``1`` and ``2``.

capfdbinary -- .../_pytest/capture.py:962
    Enable bytes capturing of writes to file descriptors ``1`` and ``2``.

doctest_namespace [session scope] -- .../_pytest/doctest.py:735
    Fixture that returns a :py:class:`dict` that will be injected into the
    namespace of doctests.

pytestconfig [session scope] -- .../_pytest/fixtures.py:1344
    Session-scoped fixture that returns the session's :class:`pytest.Config`
    object.

record_property -- .../_pytest/junitxml.py:282
    Add extra properties to the calling test.

record_xml_attribute -- .../_pytest/junitxml.py:305
    Add extra xml attributes to the tag for the calling test.

record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343
    Record a new ``<property>`` tag as child of the root ``<testsuite>``.

tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
    Return a :class:`pytest.TempdirFactory` instance for the test session.

tmpdir -- .../_pytest/legacypath.py:309
    Return a temporary directory path object which is unique to each test
    function invocation, created as a sub directory of the base temporary
    directory.

caplog -- .../_pytest/logging.py:487
    Access and control log capturing.

monkeypatch -- .../_pytest/monkeypatch.py:29
    A convenient fixture for monkey-patching.

recwarn -- .../_pytest/recwarn.py:29
    Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.

tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:184
    Return a :class:`pytest.TempPathFactory` instance for the test session.

tmp_path -- .../_pytest/tmpdir.py:199
    Return a temporary directory path object which is unique to each test
    function invocation, created as a sub directory of the base temporary
    directory.


-------------------------------------------------------------------------- fixtures defined from src.test.conftest --------------------------------------------------------------------------
fixture_module [module scope] -- src/test/conftest.py:14
    no docstring available

fixture_session [session scope] -- src/test/conftest.py:19
    no docstring available

fixture_function -- src/test/conftest.py:4
    no docstring available

fixture_class [class scope] -- src/test/conftest.py:9
    no docstring available


------------------------------------------------------------------------ fixtures defined from src.test.test_mockito ------------------------------------------------------------------------
after -- src/test/test_mockito.py:10
    no docstring available


------------------------------------------------------------------------ fixtures defined from src.test.test_pytest2 ------------------------------------------------------------------------
func1 -- src/test/test_pytest2.py:16
    no docstring available

func2_name -- src/test/test_pytest2.py:31
    no docstring available

before_after -- src/test/test_pytest2.py:5
    no docstring available


--------------------------------------------------------------------- fixtures defined from src.test.test_passing_when ----------------------------------------------------------------------
unstub_after_test -- src/test/test_passing_when.py:7
    no docstring available


=================================================================================== no tests ran in 0.49s ===================================================================================

There is no when and unstub functions in the result above but we can install pytest-mockito to add them to the predefined fixtures. They will be on the list after the installation.

$ pytest --fixtures
=================================================================================== test session starts ====================================================================================
platform linux -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: /workspaces/blogpost-python
plugins: mockito-0.0.4
collected 40 items                                                                                                                                                                         
...

----------------------------------------------------------------------- fixtures defined from pytest_mockito.plugin ------------------------------------------------------------------------
unstub -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:12
    no docstring available

when -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:20
    no docstring available

when2 -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:26
    no docstring available

expect -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:32
    no docstring available

patch -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:39
    no docstring available

spy2 -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:45
    no docstring available

unstub_ -- ../../usr/local/lib/python3.10/site-packages/pytest_mockito/plugin.py:6
    no docstring available

...
================================================================================== no tests ran in 0.49s ===================================================================================

Once the plugin is installed, we can write the same code in the following way.

import pytest

from pathlib import Path

def test_1():
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "data123"


def test_2(when):
    when(Path).read_text().thenReturn("dummy data1")
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "dummy data1"


def test_3(when):
    when(Path).read_text().thenReturn("dummy data2")
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "dummy data2"


def test_4():
    filepath = Path.joinpath(Path(__file__).parent, "test_file.txt")
    result = Path.read_text(filepath)
    assert result == "data123"

Fixture to restore the stubbed function is no longer necessary. when parameter can be used without defining any fixture.

Related articles

Comments

Copied title and URL