conftest.py is your secret weapon for sharing fixtures across multiple test files in pytest.
Imagine you have a database connection, a temporary directory, or a pre-configured web driver that multiple tests need. Instead of copying and pasting the setup code into each test file, you define it once in conftest.py and pytest automatically discovers and makes it available to all tests in that directory and its subdirectories.
Let’s see it in action.
First, create a conftest.py file in your test directory (e.g., tests/conftest.py):
# tests/conftest.py
import pytest
import tempfile
import shutil
import os
@pytest.fixture(scope="session")
def temp_dir():
"""Creates a temporary directory for the test session."""
path = tempfile.mkdtemp()
print(f"\nCreated temporary directory: {path}")
yield path
print(f"\nRemoving temporary directory: {path}")
shutil.rmtree(path)
@pytest.fixture(scope="function")
def create_file(temp_dir):
"""Creates an empty file within the temporary directory for each test function."""
def _create_file(filename="test.txt", content=""):
filepath = os.path.join(temp_dir, filename)
with open(filepath, "w") as f:
f.write(content)
print(f"Created file: {filepath}")
return filepath
return _create_file
Now, in separate test files, you can use these fixtures without redefining them.
Create tests/test_module_a.py:
# tests/test_module_a.py
def test_file_creation_a(create_file):
filepath = create_file("file_a.txt", "Content for file A")
assert os.path.exists(filepath)
with open(filepath, "r") as f:
assert f.read() == "Content for file A"
def test_another_file_a(create_file):
filepath = create_file("another_a.txt")
assert os.path.exists(filepath)
assert os.path.getsize(filepath) == 0
And tests/test_module_b.py:
# tests/test_module_b.py
def test_file_creation_b(create_file):
filepath = create_file("file_b.log", "Log entry 1\nLog entry 2")
assert os.path.exists(filepath)
with open(filepath, "r") as f:
assert f.read().startswith("Log entry 1")
def test_empty_file_b(create_file):
filepath = create_file() # Uses default filename "test.txt"
assert os.path.exists(filepath)
assert os.path.getsize(filepath) == 0
When you run pytest in your project’s root directory, pytest will discover conftest.py and make temp_dir and create_file available to all tests.
The scope parameter in a fixture is crucial. scope="session" means the fixture is set up once for the entire test run. scope="function" means it’s set up and torn down for each individual test function that uses it. In our example, temp_dir is session-scoped so the directory is created only once, while create_file is function-scoped, ensuring each test gets a clean slate within that directory.
The yield keyword in a fixture is where the setup ends and the test execution begins. Anything after yield is part of the teardown process, executed after the tests that use the fixture have finished.
You can also define fixtures in conftest.py that depend on other fixtures. In create_file, we depend on temp_dir to know where to create the file. Pytest handles this dependency resolution automatically.
A key insight is that pytest’s fixture discovery is hierarchical. A conftest.py in a subdirectory will override or extend fixtures defined in a parent conftest.py. This allows you to have project-wide fixtures, module-specific fixtures, and even directory-specific fixtures.
When you have multiple conftest.py files, pytest traverses upwards from the test file’s directory to find them. A conftest.py in the same directory as the test file has the highest priority, followed by parent directories.
The next logical step is understanding how to parameterize fixtures to run tests with different configurations.