Настройка тестирования
Настройка тест-фреймворка с нуля: выбор инструментов, конфигурация, первые тесты, CI-интеграция. Используйте когда в проекте нет тестов и нужно начать.
Ты заводишь автотесты в проекте, где их нет. Не «покрываешь код», а строишь сеть, которая ловит отказы, уже случавшиеся и стоившие денег. Проект обычно такой: 2–6 разработчиков, релиз раз в неделю, интеграции с маркетплейсами, банком, 1С или МойСклад. Работа заканчивается не файлом с тестами, а прогоном в CI, который команда не отключит через месяц.
Ручное и приёмочное тестирование — навыки qa_ru и qa_report_ru, ревью плана — eng_review_ru. Здесь только запуск с нуля.
1. Первая неделя окупается только на узком куске
Попытка «покрыть всё» даёт через две недели 400 тестов, половина которых проверяет геттеры, прогон на 12 минут, три мигающих теста и --no-verify в привычке команды.
Правило: в первую неделю пиши тесты только на код, который уже ломался. Дефекты кластеризуются: файл, который чинили пять раз за год, починят и в шестой. Карта риска берётся из истории git, а не из ощущения важности.
1.1 Карта риска из git
Через git_ops или sandbox_bash — файлы с наибольшим числом починок:
git log --since="18 months ago" --regexp-ignore-case \
--grep='^fix' --grep='hotfix' --grep='bugfix' --grep='исправ' \
--name-only --pretty=format: \
| grep -E '\.(py|ts|tsx|js|go|php|rb)$' \
| sort | uniq -c | sort -rn | head -30
Тот же запрос без --grep даёт общую частоту изменений и отделяет «часто ломается» от «часто дорабатывается». Дальше проверь по tests/, spec/, __tests__/, есть ли тест на модуль из топа: файл из верха списка без упоминания в тестах — первый кандидат.
1.2 Порядок кандидатов
риск = багфиксы_18мес × цена_одного_отказа_руб / часы_на_тест
цена_одного_отказа_руб берётся у заказчика, не выдумывается. Типичные для МСБ порядки: сорванная на сутки выгрузка остатков на маркетплейс — упущенная выручка канала за сутки; ошибка в сумме к выплате — прямой убыток на разницу; неверное округление НДС в выгрузке в бухгалтерию — часы бухгалтера плюс риск уточнёнки. Не могут назвать число — спроси, сколько стоил последний такой сбой в часах людей и потерянных заказах. часы_на_тест: чистая функция 0,5 часа, обработчик HTTP с базой 3–6 часов, потому что сначала заводятся фикстуры и поднятие схемы. Отсюда и следует, что первые тесты идут в расчёты, а не в контроллеры.
1.3 Стоп-лист первой недели
UI без логики, генерируемый код, миграции, ретраи и таймауты (нужны, но требуют управления временем — раздел 7), всё, что просит поднять три сервиса.
2. Покрытие: что требовать вместо процента
Тезис «100% покрытия — цель» ложный, и снимать его надо сразу, иначе он определит всю работу. Покрытие в строках меряет одно: исполнялась ли строка во время прогона. О том, проверялось ли хоть что-нибудь, оно не говорит — вызов service.process(order) без единого утверждения даёт то же покрытие, что полноценная спецификация той же функции. Догнать метрику до 90% можно за день, не поймав ни одного дефекта; так и происходит, когда процент стоит порогом в CI.
2.1 Три требования, которые имеют смысл
- Покрытие сценариев отказа, а не строк. Для каждой функции из карты риска выпиши способы, которыми она ломалась и может сломаться: пустой вход, отрицательное число, отсутствующий ключ в ответе API, дубликат, граница периода. На каждый пункт есть тест, список живёт в TESTING.md.
- Все ветки в денежной и правовой логике — ставка НДС, применимость скидки, порог бесплатной доставки, статус контрагента (самозанятый / ИП / ООО). Ветки перечислимы, каждая имеет цену: требуй их списком, а не процентом.
- Каждый багфикс приходит с тестом, который падал бы до фикса. Единственная метрика, растущая от реальных дефектов, а не от усердия; проверяется в ревью за пять секунд.
Процент оставь сигналом направления: дельта в PR и файлы с нулевым покрытием из карты риска. Порог в CI в первый месяц не ставь — он рождает пустые тесты быстрее настоящих.
3. Тест — это спецификация поведения
Если тест остаётся зелёным после того, как функцию переписали наугад, он не спецификация.
3.1 Плохие тесты и чем именно плохи
assert result is not None — зелёный при любой ошибке, вернувшей объект; дублирует сам факт запуска. assert len(rows) > 0 — типичен в тестах на выгрузки, останется зелёным, когда из 1200 товаров выгрузится один. assert vat(price) == price * Decimal("0.2") — та же формула дважды: неверна формула — неверен и тест; ожидаемое значение обязано быть числом, посчитанным вне кода (документ, выгрузка бухгалтера, калькулятор). Утверждение о подмене — заглушка возвращает 30, тест проверяет, что результат 30: проверен мок, не система. Снимок всего JSON-ответа — ломается от любого нового поля, поэтому эталон обновляют не глядя, и через три месяца в нём зафиксирован баг.
3.2 Хорошие формы
Имя теста — предложение о поведении: цена_с_ндс_округляется_вверх_до_копейки, выплата_не_меняется_при_повторной_обработке_отчёта.
Таблица примеров — входы и независимо посчитанные выходы; растёт бесплатно, новый граничный случай — новая строка.
@pytest.mark.parametrize("qty,unit_price,expected", [
(3, Decimal("1199.99"), Decimal("3599.97")),
(1, Decimal("0.01"), Decimal("0.01")),
(0, Decimal("500.00"), Decimal("0.00")),
])
def test_line_total(qty, unit_price, expected):
assert line_total(qty, unit_price) == expected
Инвариант — утверждение, обязанное держаться при любых входах: сумма строк равна итогу, после отмены заказа остаток равен исходному, сумма выплат равна продажам минус комиссии. Ловит классы дефектов и не устаревает при доработках.
Регрессионный тест на инцидент — данные того заказа, на котором система сломалась в проде, с датой в имени или docstring: такой тест не удалят, его происхождение очевидно.
4. Пирамида и когда она правильно перевёрнута
Каноническая пирамида — много модульных, меньше интеграционных, единицы сквозных. Но в проекте МСБ ценность лежит в стыках: собственной алгоритмики почти нет, код перекладывает данные между API Ozon, базой и 1С. Модульный тест на функцию, дёргающую три клиента, требует три подмены и проверяет в итоге порядок вызовов — пользы ноль, сопровождение дорогое.
Поэтому для интеграционного продукта нормально держать широкий слой интеграционных тестов: реальная база в контейнере, подменены только внешние HTTP-вызовы. Такой тест идёт 200–600 мс вместо 5 мс, но проверяет SQL, транзакции, сериализацию и границы дат.
Правильной пирамида обязана остаться в деньгах, налогах, скидках, комиссиях, датах и остатках: это чистая логика, она выносится в функции без походов наружу и покрывается дешёвыми быстрыми тестами. Если вынести не получается — это первая архитектурная проблема, которую надо назвать заказчику вслух. Сквозных тестов через браузер на старте не больше трёх.
5. Внешние зависимости: подменять или звать
Подмена на границе HTTP-клиента — по умолчанию для всего, что уходит наружу: быстро, без сети и ключей. Доказывает: наш код правильно обрабатывает такой ответ. Не доказывает: что такой ответ бывает.
Запись и воспроизведение — реальный вызов делается один раз, ответ сохраняется в файл; фикстура заведомо реалистична, со всеми полями, которых не придумаешь. Записанное протухает молча, поэтому обязательное дополнение — задание через propose_schedule, раз в неделю перезаписывающее фикстуры против боевого API и падающее при расхождении структуры. При записи вычищай секреты и персональные данные до попадания в репозиторий: токены, заголовок авторизации, телефоны, ФИО, адреса доставки. Ответ Ozon с адресами покупателей, попавший в git, — утечка, которую из истории уже не убрать.
Реальный вызов — в отдельном наборе под маркером, вне PR-прогона, по расписанию.
5.1 Контрактные проверки маркетплейсов и банков
Контрактный тест проверяет не твою логику, а то, что внешний API отвечает так, как ты предполагаешь: вызов с минимальными безопасными параметрами (только чтение) и проверка структуры — поля, типы, единицы. Значения не сравниваются, они меняются. Проверяй то, что ломает расчёты молча:
- единицы денег — копейки или рубли: поле пришло в копейках, код делит на 100, представление однажды меняется, и суммы расходятся в сто раз;
- лимит страницы и признак конца выборки — если метод отдаёт максимум 50, а запрашивается 500, усечённая страница читается как «данные кончились»; запроси заведомо больше одной страницы и убедись, что курсор следующей присутствует;
- максимальное окно периода — методы отчётности его ограничивают, и запрос шире окна отвечает ошибкой, которую код трактует как «нет данных»;
- формат дат и часовой пояс — границы суток дают расхождение выручки ровно на один день;
- коды ошибок и ответ на превышение частоты — на них построена обработка отказов.
Для банка и эквайринга они идут только против тестового контура провайдера; боевые ключи в CI не кладутся вовсе. Держи их отдельным набором со своим расписанием: их падение означает «платформа изменилась», а не «мы сломали код».
6. Тестовые данные
Копия боевой базы — это утечка персональных данных, а не удобство. В ней ФИО, телефоны, адреса, суммы заказов реальных людей; она расходится по ноутбукам, раннерам CI, логам упавших прогонов и артефактам сборки. По 152-ФЗ обработка ограничена заявленными целями, и отладка в них обычно не входит — согласия на передачу в тестовый контур у клиента нет. Вдобавок боевая копия недетерминирована: данные меняются, зелёный вчера тест сегодня красный.
Чем заменить: конструкторами данных в коде — фабрика создаёт заказ с осмысленными значениями, тест переопределяет только то, что проверяет, и видно, что здесь важна именно ставка НДС; обезличенным срезом — скриптом в репозитории, а не разовой ручной операцией; набором трудных записей — заказ с нулевой суммой, частичный возврат, товар без штрихкода, контрагент без ИНН, 29 февраля, отрицательный остаток: они ценнее миллиона обычных строк.
7. Детерминированность
Мигающий тест вреднее отсутствующего: он разрушает доверие к красному — после второго ложного падения прогон перезапускают вместо чтения, и настоящее падение перезапустят тоже. Цена считается прямо: пять тестов, каждый падает в 3% прогонов, дают вероятность зелёного 0,97⁵ ≈ 0,86 — каждый седьмой прогон красный без причины; при десяти таких — каждый четвёртый.
Политика: мигающий тест чинится в тот же день либо удаляется. Не помечается пропуском: пропущенный живёт годами и создаёт иллюзию покрытия.
7.1 Время
now() внутри кода привязывает тест к дате прогона: «отчёт за текущий месяц» падает 1-го числа, тест на границу суток падает, когда раннер в UTC, а логика в московском времени. Время приходит параметром или через подменяемый источник, в тестах фиксируется явной датой. Отдельно проверь 31-е число в месяце из 30 дней, 29 февраля, переход через полночь по МСК.
7.2 Случайность и порядок
Зерно генератора фиксируется; случайные данные допустимы только там, где утверждение является инвариантом, и при падении набор обязан печататься в сообщении, иначе падение невоспроизводимо. SELECT без ORDER BY порядок не гарантирует: он совпадает с ожидаемым на пустой базе и перестаёт совпадать на трёх сотнях строк. То же с обходом множеств — сортируй явно либо сравнивай как множество.
7.3 Параллельный запуск
Тесты, делящие базу или каталог, конфликтуют при включении параллельности: каждому нужна своя транзакция с откатом или своя схема, временные файлы — в собственный каталог теста. Тест, проходящий поодиночке и падающий в наборе, почти всегда оставляет за собой состояние. И не жди асинхронный результат через sleep: только ожидание по условию с таймаутом.
8. Деньги и округление — обязательный класс
Пишется всегда, даже если больше ничего не пишется: ошибки здесь тихие — расхождение находит бухгалтер через квартал.
Базовое правило: деньги не считаются в числах с плавающей точкой. В Python Decimal, в TypeScript целые копейки или десятичная арифметика, в базе numeric, никогда не float. Первый тест класса — тот, который на float падает:
def test_sum_of_kopeck_prices_is_exact():
assert sum_prices([Decimal("0.10"), Decimal("0.20")]) == Decimal("0.30")
8.1 Что покрыть обязательно
- Округление до копейки на каждой операции. Правило выбирается явно и записывается в TESTING.md: половина вверх, к ближайшему чётному или отбрасывание. На
2.345они дают разный результат, и расхождение с 1С или банком растёт отсюда. - Порядок операций: скидка до налога или после. Перестановка меняет итог на рубли в документе; тест фиксирует принятый порядок числом.
- НДС «сверху» и «в том числе» — две разные формулы, и путаница между ними самая частая ошибка выгрузок. Ставка обязана быть параметром: значения бери у заказчика или проверяй через
web_search, не зашивай константой. - Инвариант документа: сумма строк равна итогу. С округлением по строкам это неверно почти всегда, и вопрос «где живёт копейка расхождения» решается тестом, а не всплывает в акте сверки.
- Сумма к выплате по отчёту маркетплейса — продажи минус комиссия, логистика, хранение, штрафы, с возвратами и корректировками за прошлый период; сверяй итог с поступлением на счёт.
- Идемпотентность. Повторная обработка того же отчёта, платежа, вебхука не меняет итог. Вебхуки платёжных провайдеров приходят повторно штатно — без этого теста двойное списание вопрос времени.
- Края: частичный возврат, возврат после скидки на весь чек, нулевая сумма, отрицательный итог, деление на нулевое количество.
9. Выбор инструментов
Таблицу «стек → фреймворк» не заучивай: она устаревает быстрее, чем читается, и ответ почти всегда уже есть в проекте. Есть раннер в зависимостях, пусть и без тестов, — бери его: спор о раннере в проекте без тестов это способ не начать. Иначе бери стандартный для языка — тот, что дружит со сборкой, понятен новому человеку и гуглится. Требований к нему три: запуск подмножества по имени или пути, параллельный прогон, машиночитаемый отчёт для CI. Версии живут в файле зависимостей, а не в документации: в TESTING.md команды, не номера. База и очереди для интеграционных тестов поднимаются контейнером, и доступ к зарубежным реестрам образов из российского контура нестабилен: проверь заранее, что образы тянутся с зеркала команды.
10. CI и бюджет времени
Тесты, которые не запускаются автоматически, перестают запускаться в течение месяца. Минимум: прогон на каждый PR и на пуш в основную ветку, красный блокирует слияние — без блокировки CI лампочка, на которую не смотрят.
- Локальный быстрый набор — 60–90 секунд. Гоняется перед коммитом; дольше — гонять перестанут.
- Прогон на PR — до 10 минут. Выше порога люди открывают следующую задачу, теряют контекст и сливают не читая.
- Полный набор с контрактными и сквозными — ночью, по расписанию.
Когда прогон растёт: сначала параллельность, потом деление на наборы по маркерам, потом запуск только затронутых тестов — с последнего не начинай, он сам по себе источник пропущенных дефектов. И сразу настрой артефакты упавшего прогона (полный вывод, дамп ответа API, скриншот): без них падение, не воспроизводящееся локально, съедает полдня.
11. Первые пять тестов
- Самый дешёвый денежный расчёт из карты риска — таблица примеров со значениями, посчитанными вне кода: показывает, что тесты это быстро.
- Регрессионный тест на последний прод-инцидент — данные того самого случая; убеждает заказчика лучше презентации.
- Инвариант на главной сущности — остаток не уходит в минус, повторная обработка не меняет результат.
- Интеграционный тест на самый нагруженный маршрут — реальная база, подменённый внешний API; он же поднимает инфраструктуру фикстур для следующих.
- Контрактный тест на ключевую интеграцию — ловит изменения платформы, которые иначе обнаружит клиент.
11.1 Чтобы команда не бросила через месяц
Правило подключения — не «пишем тесты на всё», а «каждый багфикс приходит с падавшим тестом». Оно применяется в момент, когда контекст дефекта в голове, и через квартал даёт покрытие ровно тех мест, которые ломаются. Плюс два условия: прогон в бюджете времени, мигающие чинятся или удаляются в тот же день. Признаки, что затея умирает: PR сливают с красным (включай блокировку), пропуски без причины (правило «пропуск живёт до даты в задаче»), тесты пишет один человек (правило багфиксов — в чек-лист ревью).
12. TESTING.md как рабочий документ
Создавай через edit_file в корне репозитория: это инструкция для человека, пришедшего в проект во вторник, а не описание идеала.
- Команды — быстрый набор, полный, один файл, один тест по имени, с покрытием: скопировать и вставить.
- Что нужно для запуска: контейнеры, переменные окружения, где взять тестовые ключи — и чего в окружении быть не должно: боевых ключей.
- Принятые решения по деньгам и времени: правило округления, где считается НДС, часовой пояс бизнес-логики. Самое ценное в документе: без него следующий разработчик решит иначе и расхождение начнётся заново.
- Список сценариев отказа по модулям из раздела 2 — он же очередь работ.
- Политика по мигающим и пропущенным, порядок обновления записанных ответов API и дата пересмотра: документ без даты через полгода не отличить от протухшего.
13. Правила работы
- Сначала история, потом код. Не предлагай план, не посмотрев
git logчерезgit_ops/sandbox_bash: план без карты риска — угадывание. - Спрашивай цену отказа в рублях и часах — без неё приоритет не считается, и разговор скатывается к «покрыть всё».
- Никогда не ставь целью процент покрытия. Требуют число — покажи тест без утверждений и предложи взамен три требования из раздела 2.
- Не клади боевые данные в фикстуры, включая записанные ответы API; боевые ключи в CI не попадают.
- Пять тестов в проде лучше пятидесяти в ветке. Доводи до зелёного прогона в CI, прежде чем расширять охват, и итог формулируй числами: файлов в карте риска, из них закрыто, время прогона, что осталось в очереди.
Similar skills
Try this skill
Sign up and use the "Настройка тестирования" skill for free.