Pytest Essentials: Test Discovery, Fixtures, and Parametrization
pytest is a feature‑rich Python testing framework that simplifies writing and running tests. It provides automatic test discovery, powerful fixture management, and built‑in support for parameterized testing.
Installation
Install pytest using pip:
pip install -U pytest
pytest --version
Often used plugins can be captured in a requirements.txt file:
pytest
pytest-xdist
pytest-ordering
pytest-rerunfailures
allure-pytest
pyyaml
requests
Then install them all at once:
pip install -r requirements.txt
Test Discovery and Customisation
By default, pytest locates test modules named test_*.py or *_test.py, classes prefixed with Test (without an __init__ method), and functions or methods starting with test_.
You can override these conventions through a pytest.ini file placed at the project root:
[pytest]
testpaths = ./tests
python_files = a*
python_classes = b*
python_functions = c*
Running Tests
Command Line
Navigate to your project directory and execute:
pytest
Via pytest.main()
Create a runner script, e.g. run_tests.py:
import pytest
if __name__ == "__main__":
pytest.main(["-vs"])
# Target a specific test
# pytest.main(["-vs", "test_module.py::TestSuite::test_case"])
Using pytest.ini
The configuration file is always loaded. Common settings:
[pytest]
addopts = -vs -m "smoke"
testpaths = ./tests
markers =
smoke: smoke tests
regression: full regression suite
Setup and Teardown Hooks
pytest provides several levels of setup/teardown hooks. The following examples use print statements to illustrate execution order.
Module level – runs once per module.
def setup_module():
print("before all tests in module")
def teardown_module():
print("after all tests in module")
Function level – applies to standalone test functions (not inside a class).
def setup_function():
print("before each function")
def teardown_function():
print("after each function")
Class level – once before and after all tests in a class.
class SampleTests:
@classmethod
def setup_class(cls):
print("setup once for the class")
@classmethod
def teardown_class(cls):
print("teardown once for the class")
Method level – around each test method.
class SampleTests:
def setup_method(self, method):
print(f"before method {method.__name__}")
def teardown_method(self, method):
print(f"after method {method.__name__}")
Instance‑level setup/teardown (similar to setup_method/teardown_method but defined as plain methods).
class SampleTests:
def setup(self):
print("preparing test instance")
def teardown(self):
print("cleaning up test instance")
Fixtures
Fixtures offer reusable setup logic. Their scope is controllled by the scope parameter: function (default), class, module, or session.
import pytest
@pytest.fixture
def sample_dataset():
return ["alpha", "beta", "gamma"]
@pytest.fixture
def processed_data(sample_dataset):
return [item.upper() for item in sample_dataset]
def test_conversion(processed_data):
assert processed_data == ["ALPHA", "BETA", "GAMMA"]
Optional parameters like autouse=True activate a fixture automatically for every test.
Skipping Tests
Unconditionally skip:
@pytest.mark.skip(reason="not implemented yet")
def test_draft_feature():
pass
Conditionally skip:
import sys
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10+")
def test_new_syntax():
pass
Assertions
pytest uses the standard assert statement and provides clear failure messages.
def test_math():
assert 2 + 2 == 4
assert "pytest" in "pytest framework"
assert isinstance(42, int)
def test_exception():
import pytest
with pytest.raises(ZeroDivisionError):
1 / 0
Parametrization
Use @pytest.mark.parametrize to run a test function with multiple input sets.
import pytest
def add(a, b):
return a + b
@pytest.mark.parametrize(
"x, y, expected",
[
(1, 2, 3),
(-1, 5, 4),
(0, 0, 0),
],
)
def test_add(x, y, expected):
assert add(x, y) == expected
Test Reports
Generate a JUnit‑style XML report:
pytest --junitxml=report.xml
For an HTML report (requires pytest-html):
pytest --html=report.html