Тестирование на pytest
Тестирование на pytest
pytest - автотесты в Python-проекте
Вы изменили функцию «нормализации заголовка» — и через неделю форма на сайте перестала сохранять данные. Вручную проверять десятки сценариев после каждого коммита утомительно.
Автотест — скрипт на Python, который:
- вызывает ваш код или HTTP API;
- сравнивает результат с ожиданием;
- печатает зелёный (всё ок) или красный (что-то сломалось).
Запуск одной командой pytest занимает секунды.
В Python есть встроенный unittest (классы TestCase). На практике чаще берут pytest: меньше шаблонного кода, удобные фикстуры, понятный вывод при падении.
| Где применять | Пример в энциклопедии |
|---|---|
| Чистые функции | normalize_title(), add_task() |
| HTTP API | FastAPI 3432, Flask 3411 |
| Django / DRF | pytest-django, APIClient |
Словарь
| Термин | Простыми словами |
|---|---|
| Тест | Функция test_*, которая что-то проверяет |
| assert | «Утверждение»: если ложь — тест провален |
| Фикстура (fixture) | Подготовка перед тестом (БД, клиент API) |
| parametrize | Один тест — много наборов входных данных |
| TestClient | «Виртуальный браузер» к FastAPI без сети |
| coverage | Какой % кода хотя бы раз выполнился в тестах |
Что получится
Папка tests/ с идеями:
- простой
assertдля функции; - таблица примеров через
parametrize; - запросы к API через TestClient.
Установка и запуск
pip install pytest
Структура проекта:
myapp/
main.py
logic.py
tests/
test_logic.py
test_api.py
Pytest сам находит файлы test_*.py и функции test_* внутри них.
pytest # все тесты
pytest -q # краткий вывод
pytest -v # имя каждого теста
pytest tests/test_logic.py::test_add_positive # один тест
Запускайте из корня проекта, где видны и logic.py, и tests/. Иначе from logic import ... может дать ModuleNotFoundError.
Первый тест — чистая функция
Код приложения logic.py:
def normalize_title(title: str) -> str:
return title.strip()
Тест tests/test_logic.py:
from logic import normalize_title
def test_normalize_strips_spaces():
assert normalize_title(" hello ") == "hello"
def test_normalize_empty_becomes_empty():
assert normalize_title(" ") == ""
Разбор assert:
assert normalize_title(" hello ") == "hello"
Читается: «я утверждаю, что выражение слева равно выражению справа». Если нет — pytest покажет оба значения:
AssertionError: assert ' hello' == 'hello'
В unittest писали бы self.assertEqual(a, b) внутри класса. В pytest достаточно обычного assert.
parametrize — одна функция, много кейсов
Вместо копирования трёх функций — таблица входов и ожиданий:
import pytest
from logic import normalize_title
@pytest.mark.parametrize(
"raw, expected",
[
("a", "a"),
(" b ", "b"),
("\tc\n", "c"),
],
)
def test_normalize_examples(raw, expected):
assert normalize_title(raw) == expected
| Элемент | Смысл |
|---|---|
@pytest.mark.parametrize | Декоратор pytest |
"raw, expected" | Имена аргументов теста |
| список кортежей | Три прогона = три строки в отчёте |
raw, expected | Подставляются в функцию автоматически |
Добавить граничный случай = одна строка в списке, а не новый файл.
Фикстуры — общая подготовка
Если десяти тестам нужен один и тот же «список заметок», копировать создание списка неудобно. Выносите в fixture:
import pytest
@pytest.fixture
def sample_notes():
return [{"id": 1, "text": "learn pytest"}]
def test_list_has_one_item(sample_notes):
assert len(sample_notes) == 1
assert sample_notes[0]["text"] == "learn pytest"
Как это работает:
- Pytest видит аргумент
sample_notesв тесте. - Ищет фикстуру с таким именем.
- Вызывает
sample_notes()перед тестом. - Передаёт результат в тест.
Фикстура с yield — и teardown
@pytest.fixture
def temp_file(tmp_path):
p = tmp_path / "data.txt"
p.write_text("hello", encoding="utf-8")
yield p
# после теста файл в tmp_path удалится автоматически
Код до yield — setup, после — cleanup (закрыть соединение, откатить транзакцию).
conftest.py
Файл tests/conftest.py хранит фикстуры, общие для всей папки tests/ (клиент API, подключение к тестовой БД). Импортировать в каждом файле их не нужно — pytest подхватывает сам.
FastAPI — TestClient
Тестируем API без запуска uvicorn и без реального TCP — быстрее и стабильнее.
# tests/test_api.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_create_note():
r = client.post("/notes", json={"text": "pytest"})
assert r.status_code == 201
body = r.json()
assert body["text"] == "pytest"
assert "id" in body
def test_create_note_validation():
r = client.post("/notes", json={"text": ""})
assert r.status_code == 422
| Выражение | Смысл |
|---|---|
client.get("/health") | Симулировать GET |
r.status_code | HTTP-код: 200, 201, 404, 422 |
r.json() | Распарсенное тело JSON |
json={"text": "..."} | Тело POST + заголовок Content-Type |
TestClient поднимает ASGI-приложение в памяти того же процесса, что и тесты.
JWT в тестах
@pytest.fixture
def auth_headers(client):
r = client.post(
"/token",
data={"username": "demo", "password": "demo123"},
)
token = r.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_notes_require_auth(client):
assert client.get("/notes").status_code == 401
def test_notes_with_token(client, auth_headers):
assert client.get("/notes", headers=auth_headers).status_code == 200
Сначала получаем токен, потом передаём заголовок — как настоящий клиент после логина.
Flask — тестовый клиент
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as c:
yield c
def test_index(client):
r = client.get("/")
assert r.status_code == 200
TESTING = True отключает некоторые «боевые» эффекты (например, строгие cookies). Паттерн тот же: фикстура client, запросы get/post.
Что тестировать в первую очередь
- Бизнес-логика без HTTP — быстрые тесты, мало зависимостей.
- HTTP-контракт — коды 200, 400, 401, 422 и форма JSON.
- Coverage (
pytest --cov=myapp) — ориентир, а не цель 100 % на старте. Сначала критичные пути: оплата, регистрация, сохранение данных.
Частые ошибки
| Симптом | Причина | Решение |
|---|---|---|
ModuleNotFoundError | Запуск не из корня | cd в корень или pip install -e . |
| Тесты видят «старые» данные | Общий список в модуле мутируется | Новая фикстура на каждый тест |
| 401 в API-тестах | Нет заголовка Authorization | Фикстура auth_headers |
| Падает только в CI | Читает production .env | monkeypatch.setenv в тесте |
monkeypatch (встроенная фикстура pytest) подменяет переменные окружения на время теста.
Что попробовать
pip install pytest-cov→pytest --cov=myapp.- Встроенная фикстура
tmp_path— файлы во временной папке. pytest.raises(ValueError)— ожидаемое исключение:
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
Связанные материалы
- FastAPI — первая программа
- Flask — первая программа
- FastAPI и база данных
- Чек-лист Python (блок тестирования)
- Рекомендации по разработке
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Python как язык общего назначения - философия, ключевые свойства и области применения в современной разработке. Python — это высокоуровневый язык программирования общего назначения, который отличается читаемым синтаксисом и широким спектром применения. Принципы, которые делают код понятным, поддерживаемым и расширяемым. Примеры реализации типовых приложений. Каждый пример сопровождается разбором ключевых концепций языка. Наверняка каждый новичок, решивший перейти в что-то стандартное в Python, открывает себе этот файл. Как устроен Python, что входит в комплект и какие есть реализации. Структурные каркасы для построения приложений, как они устроены. Фреймворки, библиотеки, инструменты сборки, среды выполнения, системы тестирования и специализированные платформы, объединённые общей философией ясности, простоты и гибкости. Что такое модули, как устроены механизмы импорта и загрузки. Управление изолированной средой и зависимостями проекта. История Python - ключевые этапы развития языка, сообщества и экосистемы инструментов. Философия Python не зафиксирована в официальных стандартах, но она глубоко интегрирована в язык, его стандартную библиотеку, документацию и культуру разработчиков.Python - язык общего назначения
Что требуется знать перед началом изучения языка программирования Python
Рекомендации по разработке на Python
Простые приложения на Python
Встроенный модуль builtins и типизация в Python
Архитектура интерпретатора Python
Фреймворки и библиотеки Python
Экосистема Python-приложений
Модули в Python
Виртуальные окружения и управление зависимостями
История языка Python
Философия Python - Zen of Python