Виды тестов и дублёры в Python
Замерил цену настоящей зависимости в тестах. Поднять базу в контейнере - 2363 миллисекунды, один раз на весь набор. Один тест с откатом транзакции после себя - 1,14 миллисекунды. Тот же тест, но с полной очисткой таблицы, - 7,17. А если открывать НОВОЕ соединение на каждый тест - 94 миллисекунды, то есть в восемьдесят раз дороже самой работы.
Стержень: пирамида строится не по религиозным соображениям, а по цене обратной связи; и большую часть этой цены обычно составляет не сама зависимость, а неаккуратная подготовка.
// Формулировки: «какая должна быть пирамида тестов?», «сколько интеграционных тестов нужно?», «как ускорить набор?»
Три уровня и что каждый ловит
Модульный тест проверяет одну функцию или класс без внешних зависимостей. Быстрый, точный в указании места поломки, но ничего не знает о том, работают ли части вместе. Интеграционный проверяет связку с настоящей базой, очередью, чужим сервисом - находит именно то, что модульные пропускают: неверный запрос, расхождение схемы, забытую миграцию. Сквозной проходит весь путь пользователя целиком и ловит проблемы сборки и конфигурации.
Чем выше уровень, тем дороже поддержка и тем труднее понять, ЧТО именно сломалось. Отсюда обычная форма пирамиды: много модульных, заметно меньше интеграционных, единицы сквозных - на самые важные сценарии.
// Но пропорции не догма. Если в сервисе почти нет логики, а есть тонкий слой над базой, то модульные тесты будут проверять моки, а не поведение, и полезнее сдвинуть вес к интеграционным. Ориентир один: тест должен ловить настоящие ошибки. Тест, который никогда не краснел на реальной поломке, бесполезен независимо от уровня.
- модульный тест
- одна функция без внешних зависимостей; быстрый и точный
- интеграционный тест
- связка с настоящей базой или сервисом; ловит расхождения схемы и запросов
- сквозной тест
- весь путь пользователя; дорогой и хрупкий, поэтому единицы
Откуда берётся медленный набор
Из моего замера видно, что дорого не то, что кажется. Сама работа с настоящей базой - около миллисекунды на тест. А вот открытие нового соединения - 94 миллисекунды. На пятистах тестах это 47 секунд ровно на подключения.
Второй источник - способ уборки за собой. Откат транзакции стоил 1,14 миллисекунды, полная очистка таблицы - 7,17, то есть в шесть раз дороже. На пятистах тестах разница уже три секунды, а на настоящих схемах с десятками таблиц - десятки секунд.
// Отсюда рецепт быстрого интеграционного набора: базу поднять ОДИН раз на всю сессию, соединение переиспользовать, а изоляцию тестов делать откатом транзакции. Для сравнения: те же тесты на базе в памяти шли по 0,02 миллисекунды - в пятьдесят раз быстрее, но и ловят они меньше, потому что диалект другой.
- изоляция откатом
- каждый тест в транзакции, которая откатывается; самый дешёвый способ уборки
- переиспользование соединения
- одно соединение на набор вместо нового на каждый тест
Что проверять на каком уровне
Логику - модульными: расчёты, правила, ветвления, границы. Тут дёшево перебрать все случаи, и именно тут параметризация окупается лучше всего.
Работу с данными - интеграционными: запросы, миграции, ограничения целостности, поведение при параллельных изменениях. Мок базы не проверит ни синтаксис запроса, ни то, что колонку забыли добавить.
// Сквозными - критичные сценарии, где важна именно сборка целиком: вход в систему, оформление заказа, оплата. Их держат единицами и следят за стабильностью: мигающий сквозной тест хуже отсутствующего, потому что его перестают читать и через месяц выключают вместе с настоящими поломками.
- уровень проверки
- логика - модульно, данные - интеграционно, сценарии - сквозно
Как отвечать: «Сколько нужно интеграционных тестов и как не получить медленный набор?»
Столько, чтобы покрыть работу с данными: запросы, миграции, ограничения целостности - то, что моки не проверяют в принципе. А медленным набор делает обычно не сама база, и это хорошо видно на замерах. Я мерил: работа одного теста с настоящей базой при откате транзакции стоит около миллисекунды, а открытие нового соединения - девяносто четыре, то есть в восемьдесят раз дороже самой работы. На пятистах тестах это минута ровно на подключения. Второе место - способ уборки: откат транзакции стоил миллисекунду, полная очистка таблицы семь. Поэтому рецепт такой: базу поднимаю один раз на всю сессию, соединение переиспользую, каждый тест заворачиваю в транзакцию и откатываю. Тогда интеграционные тесты стоят почти как модульные, и спорить о пропорциях становится не о чем.
Ответ переводит вопрос с пропорций на цену обратной связи и называет настоящий источник медленности с числами. Это ровно то, что делает интеграционные тесты применимыми.
На чём валятся
- −Открывают новое соединение к базе на каждый тест: это дороже самой работы в десятки раз.
- −Чистят данные полной очисткой таблиц вместо отката транзакции.
- −Поднимают контейнер с базой заново на каждый тестовый файл.
- −Пишут модульные тесты на код, где вся суть в запросах к базе, и проверяют моки.
- −Держат десятки сквозных тестов и терпят мигание, пока их не выключат целиком.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.
- Мок, стаб и фейк — как они различаются?A)Стаб отдаёт заготовки, мок проверяет вызовы, фейк — рабочая мини-реализацияB)Отличаются только библиотекой, которой их создают в тестеC)Мок для баз данных, стаб для сети, фейк для файловой системыD)Это три названия одного и того же тестового дублёра-заглушки
показать ответ и разбор
+A)Стаб отдаёт заготовки, мок проверяет вызовы, фейк — рабочая мини-реализация// разбор: Стаб отдаёт заранее заготовленные ответы (не проверяет, как его звали). Мок сверх того проверяет взаимодействие: что метод вызвали с нужными аргументами столько-то раз. Фейк — лёгкая рабочая реализация (in-memory репозиторий вместо БД). Путаница ролей ведёт к тестам, которые проверяют не то.
- Что обычно стоит мокать в тесте, а что — нет?A)Мокать внешние границы (сеть, платёжки), а не свою бизнес-логикуB)Мокать только собственный код, а внешние сервисы дёргать вживуюC)Не мокать ничего — мок делает тест недостовернымD)Мокать вообще всё вокруг теста, чтобы он был максимально быстрым
показать ответ и разбор
+A)Мокать внешние границы (сеть, платёжки), а не свою бизнес-логику// разбор: Мокают то, что вне контроля теста и дорого/недетерминированно: внешние API, платёжные шлюзы, отправку писем, время. Собственную бизнес-логику мокать не стоит — иначе тест проверяет заглушки, а не поведение. Хорошая цель мока — граница системы, а не её сердцевина.
- Тест мокает почти всё, что зовёт функция, и всегда зелёный. Чем это плохо?A)Он не запустится в CI без доступа к внешним сервисам вживуюB)Ничем: чем больше моков, тем изолированнее и надёжнее тестC)Он проверяет сами моки, а не реальное поведение кодаD)Только тем, что такой тест дольше пишется, но результат верный
показать ответ и разбор
+C)Он проверяет сами моки, а не реальное поведение кода// разбор: Переизбыток моков превращает тест в тавтологию: ты сам задал, что вернут заглушки, и сам это проверил — реальная логика и интеграции не затронуты. Такой тест зелёный даже при сломанном коде и ломается от любого рефакторинга. Симптом — тест повторяет реализацию, а не проверяет наблюдаемый результат.
- Чем юнит-тест отличается от интеграционного?A)Разницы по сути нет, это два названия одного и того же вида автоматических тестовB)Юнит проверяет кусок в изоляции (с моками), интеграция — связку реальных компонентовC)Юнит-тесты пишут разработчики, а интеграционные — исключительно ручные тестировщикиD)Юнит-тест медленнее интеграционного, зато охватывает больше кода приложения
показать ответ и разбор
+B)Юнит проверяет кусок в изоляции (с моками), интеграция — связку реальных компонентов// разбор: Юнит-тест проверяет отдельную единицу логики в изоляции — внешние зависимости (БД, сеть, сервисы) подменяют моками; он быстрый и точечный. Интеграционный проверяет, что несколько реальных компонентов работают вместе (код + настоящая БД, два сервиса) — медленнее, но ловит проблемы стыков, которые юниты с моками пропускают. Нужны оба уровня.
- Что рекомендует «пирамида тестирования» по их соотношению?A)Много быстрых юнитов в основании, меньше интеграционных, совсем мало e2e сверхуB)Только e2e-тесты: если сквозные проходят, отдельные юниты уже писать не нужноC)Больше всего e2e-тестов, ведь они ближе всего к реальному поведению пользователяD)Поровну всех видов тестов, чтобы каждый уровень был представлен одинаково полно
показать ответ и разбор
+A)Много быстрых юнитов в основании, меньше интеграционных, совсем мало e2e сверху// разбор: Пирамида тестирования: широкое основание из быстрых, дешёвых, изолированных юнит-тестов; меньше интеграционных посередине; совсем немного медленных сквозных (e2e) на вершине. Так большинство дефектов ловится быстро и с точной локализацией, а дорогие хрупкие e2e покрывают лишь критичные пользовательские сценарии. Перевёрнутая пирамида (гора e2e) — известный антипаттерн.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.