pytest: фикстуры и параметризация тестов

12 ноября 2026
Автор

Антон Ларичев

Что такое фикстуры в pytest

Фикстура — это функция, которая подготавливает окружение для теста и при необходимости очищает его после завершения. Вместо того чтобы дублировать код инициализации в каждом тесте, вы определяете фикстуру один раз, а pytest передаёт её результат в нужные тесты через аргументы.

До появления фикстур разработчики использовали методы setUp и tearDown из unittest. pytest предлагает более гибкий и читаемый подход.

import pytest

@pytest.fixture
def user():
    return {"id": 1, "name": "Alice", "email": "alice@example.com"}

def test_user_has_name(user):
    assert user["name"] == "Alice"

def test_user_has_email(user):
    assert "@" in user["email"]

Pytest автоматически находит функцию user и передаёт её возвращаемое значение в каждый тест, который объявил параметр с таким именем.

Курс по теме

Основы Python — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Фикстуры с очисткой через yield

Если тест создаёт ресурс, который нужно освободить — соединение с базой данных, временный файл, запущенный сервер — используйте yield вместо return. Код после yield выполняется как teardown.

import pytest
import tempfile
import os

@pytest.fixture
def temp_file():
    fd, path = tempfile.mkstemp()
    os.close(fd)
    yield path
    os.unlink(path)  # выполняется после теста

def test_write_to_file(temp_file):
    with open(temp_file, "w") as f:
        f.write("hello")
    with open(temp_file) as f:
        assert f.read() == "hello"

Даже если тест завершился с ошибкой, teardown-часть будет выполнена — это гарантирует корректную очистку ресурсов.

Область видимости фикстур (scope)

По умолчанию фикстура создаётся заново для каждого теста. Это безопасно, но иногда дорого — например, поднимать базу данных перед каждым тестом нецелесообразно. Параметр scope управляет временем жизни фикстуры.

scopeФикстура создаётся
function (по умолчанию)Один раз для каждого теста
classОдин раз для каждого тестового класса
moduleОдин раз для каждого файла
packageОдин раз для каждого пакета
sessionОдин раз за весь прогон тестов
import pytest

@pytest.fixture(scope="session")
def db_connection():
    print("\nОткрываем соединение с БД")
    connection = {"host": "localhost", "port": 5432, "connected": True}
    yield connection
    print("\nЗакрываем соединение с БД")
    connection["connected"] = False

@pytest.fixture(scope="module")
def db_cursor(db_connection):
    cursor = {"connection": db_connection, "queries": []}
    yield cursor
    cursor["queries"].clear()

def test_insert(db_cursor):
    db_cursor["queries"].append("INSERT INTO users...")
    assert len(db_cursor["queries"]) == 1

def test_select(db_cursor):
    db_cursor["queries"].append("SELECT * FROM users")
    assert len(db_cursor["queries"]) > 0

Правило: фикстура не может запрашивать другую фикстуру с более узкой областью видимости. Фикстура session не может зависеть от фикстуры function.

Автоматические фикстуры (autouse)

Если нужно применить фикстуру ко всем тестам в модуле или сессии без явного указания в параметрах, используйте autouse=True.

import pytest
import time

@pytest.fixture(autouse=True)
def log_test_name(request):
    print(f"\nЗапуск: {request.node.name}")
    start = time.time()
    yield
    elapsed = time.time() - start
    print(f"Завершён за {elapsed:.3f}с")

def test_addition():
    assert 2 + 2 == 4

def test_multiplication():
    assert 3 * 4 == 12

request — встроенная фикстура pytest, которая предоставляет информацию о текущем тесте: его имя, путь к файлу, параметры и многое другое.

conftest.py — общие фикстуры

Фикстуры, нужные в нескольких файлах, размещают в conftest.py. Pytest автоматически находит и загружает этот файл — импортировать его не нужно.

project/
├── conftest.py          # фикстуры для всего проекта
├── tests/
│   ├── conftest.py      # фикстуры для всех тестов
│   ├── test_users.py
│   └── api/
│       ├── conftest.py  # фикстуры только для api-тестов
│       └── test_endpoints.py
# tests/conftest.py
import pytest

@pytest.fixture
def base_url():
    return "http://localhost:8000"

@pytest.fixture
def auth_headers():
    return {"Authorization": "Bearer test-token-123"}
# tests/test_users.py
def test_get_users(base_url, auth_headers):
    url = f"{base_url}/users"
    assert url == "http://localhost:8000/users"
    assert "Authorization" in auth_headers

Фикстуры ищутся в порядке: текущий файл → ближайший conftest.py → conftest.py выше по дереву.

Параметризация тестов через @pytest.mark.parametrize

Параметризация позволяет запустить один и тот же тест с разными входными данными. Без неё пришлось бы либо писать отдельный тест для каждого случая, либо использовать цикл, теряя детализацию при падении.

import pytest

def is_palindrome(s: str) -> bool:
    s = s.lower().replace(" ", "")
    return s == s[::-1]

@pytest.mark.parametrize("word,expected", [
    ("racecar", True),
    ("hello", False),
    ("A man a plan a canal Panama", True),
    ("level", True),
    ("world", False),
])
def test_is_palindrome(word, expected):
    assert is_palindrome(word) == expected

При запуске pytest создаст пять отдельных тестов с понятными идентификаторами:

test_palindrome.py::test_is_palindrome[racecar-True] PASSED
test_palindrome.py::test_is_palindrome[hello-False] PASSED
test_palindrome.py::test_is_palindrome[A man a plan a canal Panama-True] PASSED

Именованные параметры с pytest.param

Чтобы дать параметрам читаемые идентификаторы или пометить отдельные случаи как ожидаемо падающие, используйте pytest.param.

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Деление на ноль")
    return a / b

@pytest.mark.parametrize("a,b,expected", [
    pytest.param(10, 2, 5.0, id="простое деление"),
    pytest.param(7, 2, 3.5, id="деление с остатком"),
    pytest.param(0, 5, 0.0, id="ноль делим на число"),
    pytest.param(
        10, 0, None,
        marks=pytest.mark.xfail(raises=ValueError),
        id="деление на ноль"
    ),
])
def test_divide(a, b, expected):
    assert divide(a, b) == expected

xfail помечает тест как «ожидаемо упавший». Если он всё-таки прошёл, pytest сообщит об XPASS — неожиданном успехе.

Параметризация на уровне класса

Декоратор parametrize можно применить к классу целиком — тогда все тесты внутри получат все комбинации параметров.

import pytest

class Calculator:
    def add(self, a, b): return a + b
    def subtract(self, a, b): return a - b

@pytest.mark.parametrize("a,b", [(1, 2), (10, 20), (-5, 5)])
class TestCalculator:
    def test_add_commutative(self, a, b):
        calc = Calculator()
        assert calc.add(a, b) == calc.add(b, a)

    def test_subtract_not_commutative(self, a, b):
        calc = Calculator()
        assert calc.subtract(a, b) != calc.subtract(b, a) or a == b

Комбинирование нескольких parametrize

Если на функцию навесить несколько декораторов parametrize, pytest создаст декартово произведение всех наборов параметров.

import pytest

def format_greeting(greeting, name, punctuation):
    return f"{greeting}, {name}{punctuation}"

@pytest.mark.parametrize("punctuation", ["!", ".", "?"])
@pytest.mark.parametrize("name", ["Alice", "Bob"])
def test_format_greeting(name, punctuation):
    result = format_greeting("Hello", name, punctuation)
    assert result.startswith("Hello, ")
    assert result.endswith(punctuation)

Это создаст 6 тестов: все комбинации имён и знаков препинания.

Параметризованные фикстуры

Фикстуры тоже можно параметризовать через params. Это полезно, когда нужно прогнать одни и те же тесты с разными реализациями или конфигурациями.

import pytest

class SqliteStorage:
    def __init__(self): self.data = {}
    def set(self, key, value): self.data[key] = value
    def get(self, key): return self.data.get(key)

class RedisStorage:
    def __init__(self): self.data = {}
    def set(self, key, value): self.data[key] = value
    def get(self, key): return self.data.get(key)

@pytest.fixture(params=[SqliteStorage, RedisStorage], ids=["sqlite", "redis"])
def storage(request):
    return request.param()

def test_storage_set_get(storage):
    storage.set("key", "value")
    assert storage.get("key") == "value"

def test_storage_missing_key(storage):
    assert storage.get("nonexistent") is None

Kaждый тест запустится дважды — по одному разу для каждой реализации хранилища. Это обеспечивает покрытие одной кодовой базой тестов сразу для нескольких бэкендов.

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

Иногда нужно передать фикстуру как один из параметров в parametrize. Для этого используется специальный синтаксис с request.getfixturevalue.

import pytest

@pytest.fixture
def admin_user():
    return {"role": "admin", "permissions": ["read", "write", "delete"]}

@pytest.fixture
def regular_user():
    return {"role": "user", "permissions": ["read"]}

@pytest.mark.parametrize("user_fixture,can_delete", [
    ("admin_user", True),
    ("regular_user", False),
])
def test_delete_permission(request, user_fixture, can_delete):
    user = request.getfixturevalue(user_fixture)
    assert ("delete" in user["permissions"]) == can_delete

Пример: тестирование HTTP-клиента с фикстурами и параметризацией

Поставим задачу: нужно протестировать функцию, которая делает запросы к API. Используем pytest-httpserver для мока или просто смоделируем ситуацию через фикстуры.

import pytest
from unittest.mock import MagicMock, patch

def fetch_user(user_id: int, base_url: str) -> dict:
    import urllib.request
    import json
    url = f"{base_url}/users/{user_id}"
    with urllib.request.urlopen(url) as response:
        return json.loads(response.read())

@pytest.fixture
def mock_urlopen():
    with patch("urllib.request.urlopen") as mock:
        yield mock

@pytest.mark.parametrize("user_id,expected_name", [
    (1, "Alice"),
    (2, "Bob"),
    (3, "Charlie"),
])
def test_fetch_user(mock_urlopen, user_id, expected_name):
    mock_response = MagicMock()
    mock_response.read.return_value = f'{{"id": {user_id}, "name": "{expected_name}"}}'.encode()
    mock_response.__enter__ = lambda s: s
    mock_response.__exit__ = MagicMock(return_value=False)
    mock_urlopen.return_value = mock_response

    result = fetch_user(user_id, "http://api.example.com")

    assert result["id"] == user_id
    assert result["name"] == expected_name

Здесь фикстура mock_urlopen управляет патчингом, а parametrize покрывает разные значения без дублирования кода.

Проверка исключений с параметризацией

import pytest

def parse_age(value: str) -> int:
    age = int(value)
    if age < 0 or age > 150:
        raise ValueError(f"Недопустимый возраст: {age}")
    return age

@pytest.mark.parametrize("value,expected", [
    ("25", 25),
    ("0", 0),
    ("150", 150),
])
def test_parse_age_valid(value, expected):
    assert parse_age(value) == expected

@pytest.mark.parametrize("value,exc_type", [
    ("-1", ValueError),
    ("151", ValueError),
    ("abc", ValueError),
    ("1.5", ValueError),
])
def test_parse_age_invalid(value, exc_type):
    with pytest.raises(exc_type):
        parse_age(value)

Полезные встроенные фикстуры pytest

Pytest поставляется с набором готовых фикстур:

  • tmp_path — объект pathlib.Path, указывающий на временную директорию, уникальную для каждого теста
  • capsys — перехват stdout/stderr
  • monkeypatch — временная замена атрибутов, переменных окружения, функций
  • caplog — перехват логов
  • request — метаданные о текущем тесте
def greet(name: str) -> str:
    print(f"Hello, {name}!")
    return f"Hello, {name}!"

def test_greet_output(capsys):
    greet("World")
    captured = capsys.readouterr()
    assert captured.out == "Hello, World!\n"

def test_env_variable(monkeypatch):
    monkeypatch.setenv("APP_ENV", "testing")
    import os
    assert os.environ["APP_ENV"] == "testing"

def test_tmp_file(tmp_path):
    file = tmp_path / "data.txt"
    file.write_text("content")
    assert file.read_text() == "content"

Советы по организации тестов

Несколько практик, которые делают тестовую кодовую базу удобной:

Группируйте фикстуры по слоям. Низкоуровневые фикстуры (соединение с БД) помещайте в conftest.py корневого уровня, доменные (объекты пользователей, заказов) — в conftest.py рядом с соответствующими тестами.

Называйте параметры говорящими именами. Вместо (1, True) пишите pytest.param(1, True, id="single item returns True").

Не злоупотребляйте autouse. Это скрытая зависимость — о ней сложно узнать, не читая conftest.py. Используйте только для сквозной логики: логирование, таймеры, глобальные моки.

Разделяйте setup и assert. Фикстура отвечает за подготовку состояния, тест — только за проверку. Если фикстура содержит assert, она выполняет слишком много работы.

# Хорошо — фикстура готовит данные, тест проверяет поведение
@pytest.fixture
def populated_cart():
    return {"items": [{"id": 1, "qty": 2}, {"id": 2, "qty": 1}], "discount": 0}

def test_cart_total(populated_cart):
    total = sum(item["qty"] for item in populated_cart["items"])
    assert total == 3

Итог

Фикстуры и параметризация — два фундаментальных инструмента pytest, которые устраняют дублирование в тестах и делают покрытие полным без лишних усилий. Фикстуры управляют жизненным циклом зависимостей: создают ресурсы перед тестом и освобождают их после. Параметризация превращает один тест в набор независимых сценариев с понятными идентификаторами при падении.

Освоив область видимости фикстур и комбинирование parametrize, вы сможете строить быстрые и надёжные тестовые наборы даже для сложных систем с множеством конфигураций.

Чтобы изучить тестирование в контексте полного Python-проекта и получить практику на реальных задачах, смотрите курс Python-разработчик на PurpleSchool.

Стрелочка влевоPython 3.13: новые возможностиPython mypy — статическая проверка типовСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОписание объектов PythonОсновы Python coreНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
uv — современный менеджер пакетов PythonTyper: создание CLI приложений на PythonPython tomllib — встроенная работа с TOML файламиPython subprocess: запуск внешних команд и процессовSQLModel ORM для FastAPIВеб-скрапинг с Scrapy в PythonPython rich: красивый вывод в терминалеМиграция с Pydantic v1 на v2Pydantic: валидация данных в PythonPoetry: управление зависимостями в PythonPython pathlib — современная работа с файловыми путямиPython с OpenAI API: от установки до потоковых ответовPython msgspec: быстрая сериализация данныхLangChain: основы работы с библиотекойPython itertools — работа с итераторамиPython click: создание CLI-приложенийПарсинг сайтов с BeautifulSoup в PythonPython argparse: разбор аргументов командной строкиAlembic для миграций базы данных с SQLAlchemy
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonОбработка чисел, введённых через input в PythonЦикл while в Python и примеры использованияОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор site-packages в PythonРазбор Program Files в PythonРазбор traceback в модуле PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовИмпорт модулей в Python и правила подключенияМодули в Python и организация кода в проектеРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиПроверка на четность в PythonИспользование range в Python для цикловПреобразование типов в PythonПреобразование списка в строку PythonПостроение графиков в PythonПреобразование числа в строку в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в PythonПроверка числа в Python
Телеграм-бот на Python: от команды до деплояАсинхронная работа с базой данных через SQLAlchemy 2.0Redis в Python с asyncioPython multiprocessing: параллельные процессыMotor: асинхронная работа с MongoDB в PythonPython httpx: современный HTTP-клиентFastAPI WebSockets в реальном времениFastAPI middleware и обработка запросовFastAPI lifespan: управление ресурсами приложенияFastAPI с JWT аутентификациейФоновые задачи FastAPI — BackgroundTasksExceptionGroup и except* — группы исключений в Pythonconcurrent.futures: параллельные задачи в PythonФоновые задачи с Celery в PythonАсинхронная работа с PostgreSQL в Python через asyncpgasyncio.Runner — управление циклом событий вручнуюasyncio Queue: паттерны Producer-ConsumerPython asyncio.TaskGroupМодуль asyncio в Python — основы асинхронного программированияСинтаксис async/await в Python — как писать асинхронный кодБиблиотека aiohttp в Python — асинхронные HTTP-запросы
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий