Python · Testing with pytest · Beginner

Fixtures

5 tasks

Use @pytest.fixture for reusable setup and teardown without boilerplate.

Why Fixtures Exist and How They Work

#
**The problem fixtures solve:** Imagine five test functions that all need a populated user object. Without fixtures, you'd either repeat the setup code in each test or share a module-level variable — both are fragile. pytest fixtures solve this cleanly. A fixture is a function decorated with `@pytest.fixture`. Any test that declares it as a parameter receives the fixture's return value automatically — no import, no call, just the name: ```python import pytest @pytest.fixture def user(): return {'name': 'Alice', 'email': '[email protected]', 'active': True} def test_user_is_active(user): # pytest sees 'user' parameter → runs fixture assert user['active'] is True def test_user_has_email(user): # same fixture, separate call assert '@' in user['email'] ``` pytest resolves fixture names by matching parameter names. If a test has a parameter called `user`, pytest finds a fixture named `user` and injects it. This is *dependency injection by name*. **The yield pattern — setup and teardown:** Use `yield` instead of `return` when you need cleanup after the test finishes: ```python @pytest.fixture def temp_file(tmp_path): path = tmp_path / 'data.txt' path.write_text('hello world') # ← setup runs before the test yield path # ← test receives the path here path.unlink(missing_ok=True) # ← teardown runs after the test, even on failure ``` Everything before `yield` is setup. Everything after is teardown. It's the equivalent of `try / finally` but expressed as a linear flow. **Fixture scope — how long a fixture lives:** By default, a fixture runs once per test function (`scope='function'`). You can change this: ```python @pytest.fixture(scope='module') def db_connection(): conn = create_expensive_connection() yield conn conn.close() ``` With `scope='module'`, the fixture runs *once* for the entire test file and the connection is shared across all tests in it. This is useful for expensive setup like DB connections or loading large files. | Scope | Fixture runs once per | |---|---| | `function` (default) | each test | | `class` | each test class | | `module` | each test file | | `session` | entire `pytest` run | **One important rule:** Never call a fixture function directly in a test. Let pytest inject it: ```python # WRONG def test_something(): u = user() # this just calls the plain function, bypassing pytest # RIGHT def test_something(user): # pytest injects the fixture ... ```

Fixture Patterns You Will Use Daily

#
**Simple fixture — returns a value:** ```python import pytest @pytest.fixture def sample_scores(): return [88, 92, 75, 100, 63, 41] def test_highest_score(sample_scores): assert max(sample_scores) == 100 def test_passing_scores(sample_scores): passing = [s for s in sample_scores if s >= 60] assert len(passing) == 4 ``` Each test call runs the fixture fresh. Mutating `sample_scores` in one test does not affect the other. **Fixture with yield — setup + teardown:** ```python @pytest.fixture def temp_config(tmp_path): config_file = tmp_path / 'config.json' config_file.write_text('{"debug": true, "timeout": 30}') yield config_file # test runs here # teardown — file is already cleaned up by tmp_path, but you could do more here def test_config_readable(temp_config): content = temp_config.read_text() assert 'debug' in content def test_config_is_file(temp_config): assert temp_config.is_file() ``` **Fixture depending on another fixture:** ```python @pytest.fixture def base_url(): return 'https://api.example.com' @pytest.fixture def api_headers(): return {'Content-Type': 'application/json'} @pytest.fixture def api_client(base_url, api_headers): return {'url': base_url, 'headers': api_headers, 'timeout': 10} def test_client_config(api_client): assert api_client['url'].startswith('https') assert 'Content-Type' in api_client['headers'] assert api_client['timeout'] == 10 ``` pytest builds the dependency graph automatically. If `api_client` needs `base_url` and `api_headers`, those run first. **Module-scoped fixture for expensive setup:** ```python @pytest.fixture(scope='module') def large_dataset(): # runs once for the whole file, not once per test data = list(range(100_000)) return data def test_dataset_size(large_dataset): assert len(large_dataset) == 100_000 def test_dataset_max(large_dataset): assert max(large_dataset) == 99_999 ``` **Using built-in fixtures:** pytest provides useful fixtures for free. `tmp_path` gives you a unique temporary directory per test: ```python def test_write_and_read(tmp_path): f = tmp_path / 'output.txt' f.write_text('hello') assert f.read_text() == 'hello' ``` `monkeypatch` lets you safely replace attributes or environment variables: ```python import os def test_with_env(monkeypatch): monkeypatch.setenv('API_KEY', 'test-key-123') assert os.environ['API_KEY'] == 'test-key-123' # original env is restored automatically after the test ```

Fixture Quick Reference

#
```python import pytest # Return a value @pytest.fixture def my_fixture(): return value # Setup + teardown with yield @pytest.fixture def my_fixture(): resource = setup() yield resource # test runs here cleanup(resource) # always runs, even on test failure # Scoped fixture (runs once per module, shared by all tests in it) @pytest.fixture(scope='module') def expensive_data(): return load_big_dataset() ``` **Scope values:** | Scope | Fixture runs once per | |---|---| | `function` (default) | each test function | | `class` | each test class | | `module` | each test file | | `session` | entire `pytest` run | **Built-in fixtures (always available):** ```python tmp_path # pathlib.Path to a unique temp dir (function scope) tmp_path_factory # factory for temp dirs (for session-scoped fixtures) capsys # capture and inspect stdout/stderr monkeypatch # safely patch objects, env vars, dict entries request # info about the running test (name, module, etc.) ```
01

Write a Fixture Used by Two Tests

#

Create a `@pytest.fixture` called `numbers` that returns `[3, 1, 4, 1, 5, 9, 2, 6]`. Write two test functions that receive it as a parameter: one asserts `sum(numbers) == 31`, the other asserts `max(numbers) == 9`.

import pytest


@pytest.fixture
def numbers():
    pass  # return the list


def test_sum(numbers):
    pass

def test_max(numbers):
    pass
Solution
import pytest


@pytest.fixture
def numbers():
    return [3, 1, 4, 1, 5, 9, 2, 6]


def test_sum(numbers):
    assert sum(numbers) == 31

def test_max(numbers):
    assert max(numbers) == 9
02

Fixture with yield for Cleanup

#

Write a `@pytest.fixture` called `temp_csv` that creates a temporary CSV file with two lines of data using `tmp_path`, yields the file path, then removes the file. Write tests that read the file and verify the line count and header.

import pytest


@pytest.fixture
def temp_csv(tmp_path):
    path = tmp_path / 'data.csv'
    # write two lines to path
    yield path
    # cleanup


def test_csv_has_two_lines(temp_csv):
    pass
Solution
import pytest


@pytest.fixture
def temp_csv(tmp_path):
    path = tmp_path / 'data.csv'
    path.write_text('name,age\nAlice,30\n')
    yield path
    if path.exists():
        path.unlink()


def test_csv_has_two_lines(temp_csv):
    lines = temp_csv.read_text().strip().splitlines()
    assert len(lines) == 2

def test_csv_header(temp_csv):
    first_line = temp_csv.read_text().splitlines()[0]
    assert first_line == 'name,age'
03

Config Fixture for a Function Under Test

#

Write a `@pytest.fixture` called `app_config` returning `{'base_url': 'https://api.example.com', 'timeout': 10, 'retries': 3}`. Write `build_url(config, path)` that joins `base_url` and `path` cleanly (no double slashes). Test it using the fixture.

import pytest


@pytest.fixture
def app_config():
    pass  # return the config dict


def build_url(config, path):
    pass  # join base_url and path


def test_build_url(app_config):
    pass
Solution
import pytest


@pytest.fixture
def app_config():
    return {'base_url': 'https://api.example.com', 'timeout': 10, 'retries': 3}


def build_url(config, path):
    return config['base_url'].rstrip('/') + '/' + path.lstrip('/')


def test_build_url(app_config):
    url = build_url(app_config, '/users')
    assert url == 'https://api.example.com/users'

def test_build_url_no_double_slash(app_config):
    url = build_url(app_config, 'products')
    assert '//' not in url.split('https://')[-1]

def test_config_timeout(app_config):
    assert app_config['timeout'] == 10
04

Use Two Fixtures in One Test

#

Create two fixtures: `base_url` returning `'https://api.example.com'` and `headers` returning `{'Content-Type': 'application/json', 'Accept': 'application/json'}`. Write a test that uses both and asserts the URL is HTTPS and the headers contain the right keys.

import pytest


@pytest.fixture
def base_url():
    pass


@pytest.fixture
def headers():
    pass


def test_request_setup(base_url, headers):
    pass
Solution
import pytest


@pytest.fixture
def base_url():
    return 'https://api.example.com'


@pytest.fixture
def headers():
    return {'Content-Type': 'application/json', 'Accept': 'application/json'}


def test_request_setup(base_url, headers):
    assert base_url.startswith('https://')
    assert 'Content-Type' in headers
    assert headers['Content-Type'] == 'application/json'
    assert 'Accept' in headers
05

Fixture Chain

#

Create three fixtures in a chain: `db_config` returns a dict with `host` and `port`, `db_connection` receives `db_config` and returns `{'status': 'connected', ...}`, `db_cursor` receives `db_connection` and returns `{'query': None, 'connection': ...}`. Write a test that only requests `db_cursor`.

import pytest


@pytest.fixture
def db_config():
    pass


@pytest.fixture
def db_connection(db_config):
    pass


@pytest.fixture
def db_cursor(db_connection):
    pass


def test_cursor_ready(db_cursor):
    pass
Solution
import pytest


@pytest.fixture
def db_config():
    return {'host': 'localhost', 'port': 5432}


@pytest.fixture
def db_connection(db_config):
    return {'status': 'connected', 'host': db_config['host'], 'port': db_config['port']}


@pytest.fixture
def db_cursor(db_connection):
    return {'query': None, 'connection': db_connection}


def test_cursor_ready(db_cursor):
    assert db_cursor['query'] is None
    assert db_cursor['connection']['status'] == 'connected'
    assert db_cursor['connection']['host'] == 'localhost'