Python · Тестирование с pytest · Начальный

Фикстуры

5 задач

Используйте @pytest.fixture для переиспользуемой настройки без лишнего кода.

Зачем нужны фикстуры и как они работают

#
**Какую проблему решают фикстуры:** Представьте пять тест-функций, которым всем нужен заполненный объект пользователя. Без фикстур пришлось бы либо повторять код инициализации в каждом тесте, либо использовать переменную на уровне модуля -- оба варианта хрупки. Фикстуры pytest решают эту проблему чисто. Фикстура -- это функция, декорированная `@pytest.fixture`. Любой тест, объявляющий её как параметр, автоматически получает возвращаемое фикстурой значение -- без импорта, без явного вызова, просто по имени: ```python import pytest @pytest.fixture def user(): return {'name': 'Alice', 'email': '[email protected]', 'active': True} def test_user_is_active(user): # pytest видит параметр 'user' -> запускает фикстуру assert user['active'] is True def test_user_has_email(user): # та же фикстура, отдельный вызов assert '@' in user['email'] ``` pytest разрешает имена фикстур путём сопоставления имён параметров. Если у теста есть параметр `user`, pytest находит фикстуру с именем `user` и подставляет её. Это *внедрение зависимостей по имени*. **Паттерн yield -- инициализация и завершение:** Используйте `yield` вместо `return`, когда нужна очистка после завершения теста: ```python @pytest.fixture def temp_file(tmp_path): path = tmp_path / 'data.txt' path.write_text('hello world') # <- инициализация выполняется до теста yield path # <- тест получает path здесь path.unlink(missing_ok=True) # <- завершение выполняется после теста, даже при падении ``` Всё до `yield` -- инициализация. Всё после -- завершение. Это эквивалент `try / finally`, но выраженный в виде линейного потока. **Scope фикстуры -- как долго живёт фикстура:** По умолчанию фикстура запускается один раз для каждой тест-функции (`scope='function'`). Это можно изменить: ```python @pytest.fixture(scope='module') def db_connection(): conn = create_expensive_connection() yield conn conn.close() ``` С `scope='module'` фикстура запускается *один раз* для всего тестового файла, и соединение используется совместно всеми тестами в нём. Это полезно для дорогой инициализации, такой как соединения с БД или загрузка больших файлов. | Scope | Фикстура запускается один раз для | |---|---| | `function` (по умолчанию) | каждого теста | | `class` | каждого тестового класса | | `module` | каждого тестового файла | | `session` | всего запуска `pytest` | **Одно важное правило:** Никогда не вызывайте функцию-фикстуру напрямую в тесте. Позвольте pytest внедрить её: ```python # НЕПРАВИЛЬНО def test_something(): u = user() # это просто вызов обычной функции, минуя pytest # ПРАВИЛЬНО def test_something(user): # pytest внедряет фикстуру ... ```

Паттерны фикстур, которые используются каждый день

#
**Простая фикстура -- возвращает значение:** ```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 ``` Каждый вызов теста запускает фикстуру заново. Изменение `sample_scores` в одном тесте не влияет на другой. **Фикстура с yield -- инициализация + завершение:** ```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 # тест выполняется здесь # завершение -- файл уже очищается tmp_path, но здесь можно сделать больше 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() ``` **Фикстура, зависящая от другой фикстуры:** ```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 автоматически строит граф зависимостей. Если `api_client` нужны `base_url` и `api_headers`, они запускаются первыми. **Фикстура с module scope для дорогой инициализации:** ```python @pytest.fixture(scope='module') def large_dataset(): # запускается один раз для всего файла, а не для каждого теста 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 ``` **Использование встроенных фикстур:** pytest предоставляет полезные фикстуры бесплатно. `tmp_path` даёт уникальный временный каталог для каждого теста: ```python def test_write_and_read(tmp_path): f = tmp_path / 'output.txt' f.write_text('hello') assert f.read_text() == 'hello' ``` `monkeypatch` позволяет безопасно заменять атрибуты или переменные окружения: ```python import os def test_with_env(monkeypatch): monkeypatch.setenv('API_KEY', 'test-key-123') assert os.environ['API_KEY'] == 'test-key-123' # оригинальное окружение автоматически восстанавливается после теста ```

Краткий справочник по фикстурам

#
```python import pytest # Возврат значения @pytest.fixture def my_fixture(): return value # Инициализация + завершение через yield @pytest.fixture def my_fixture(): resource = setup() yield resource # тест выполняется здесь cleanup(resource) # выполняется всегда, даже при падении теста # Фикстура с module scope (запускается один раз, используется всеми тестами файла) @pytest.fixture(scope='module') def expensive_data(): return load_big_dataset() ``` **Значения scope:** | Scope | Фикстура запускается один раз для | |---|---| | `function` (по умолчанию) | каждой тест-функции | | `class` | каждого тестового класса | | `module` | каждого тестового файла | | `session` | всего запуска `pytest` | **Встроенные фикстуры (всегда доступны):** ```python tmp_path # pathlib.Path к уникальному временному каталогу (function scope) tmp_path_factory # фабрика временных каталогов (для фикстур с session scope) capsys # перехват и проверка stdout/stderr monkeypatch # безопасный патчинг объектов, переменных окружения, записей словарей request # информация о текущем тесте (имя, модуль и т.д.) ```
01

Фикстура, используемая двумя тестами

#

Создайте `@pytest.fixture` с именем `numbers`, которая возвращает `[3, 1, 4, 1, 5, 9, 2, 6]`. Напишите две тест-функции, принимающие её как параметр: одна проверяет `sum(numbers) == 31`, другая -- `max(numbers) == 9`.

import pytest


@pytest.fixture
def numbers():
    pass  # вернуть список


def test_sum(numbers):
    pass

def test_max(numbers):
    pass
Решение
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

Фикстура с yield для очистки

#

Напишите `@pytest.fixture` с именем `temp_csv`, которая создаёт временный CSV-файл с двумя строками данных через `tmp_path`, возвращает путь к файлу через yield, затем удаляет файл. Напишите тесты, читающие файл и проверяющие количество строк и заголовок.

import pytest


@pytest.fixture
def temp_csv(tmp_path):
    path = tmp_path / 'data.csv'
    # записать две строки в path
    yield path
    # очистка


def test_csv_has_two_lines(temp_csv):
    pass
Решение
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

Фикстура конфигурации для тестируемой функции

#

Напишите `@pytest.fixture` с именем `app_config`, возвращающую `{'base_url': 'https://api.example.com', 'timeout': 10, 'retries': 3}`. Напишите `build_url(config, path)`, которая соединяет `base_url` и `path` без двойных слешей. Протестируйте её с помощью фикстуры.

import pytest


@pytest.fixture
def app_config():
    pass  # вернуть словарь конфигурации


def build_url(config, path):
    pass  # соединить base_url и path


def test_build_url(app_config):
    pass
Решение
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

Использование двух фикстур в одном тесте

#

Создайте две фикстуры: `base_url`, возвращающую `'https://api.example.com'`, и `headers`, возвращающую `{'Content-Type': 'application/json', 'Accept': 'application/json'}`. Напишите тест, использующий обе, и проверяющий, что URL использует HTTPS, а заголовки содержат нужные ключи.

import pytest


@pytest.fixture
def base_url():
    pass


@pytest.fixture
def headers():
    pass


def test_request_setup(base_url, headers):
    pass
Решение
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

Цепочка фикстур

#

Создайте три фикстуры цепочкой: `db_config` возвращает словарь с `host` и `port`, `db_connection` принимает `db_config` и возвращает `{'status': 'connected', ...}`, `db_cursor` принимает `db_connection` и возвращает `{'query': None, 'connection': ...}`. Напишите тест, который запрашивает только `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
Решение
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'