If you want the complete code, you can go to my GitHub repository.
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.
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.
Scope | Meaning |
---|---|
function | Executed for each test case (Default) |
class | Executed once for a whole class |
module | Executed once for a whole test file |
session | Executed 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.
Scope | Call count |
---|---|
session | 1 |
module | 2 |
class | 2 |
function | 4 |
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.
Comments