сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Тесты в Python

Виды тестов и дублёры в Python

Виды тестов и их цена

Замерил цену настоящей зависимости в тестах. Поднять базу в контейнере - 2363 миллисекунды, один раз на весь набор. Один тест с откатом транзакции после себя - 1,14 миллисекунды. Тот же тест, но с полной очисткой таблицы, - 7,17. А если открывать НОВОЕ соединение на каждый тест - 94 миллисекунды, то есть в восемьдесят раз дороже самой работы.

Стержень: пирамида строится не по религиозным соображениям, а по цене обратной связи; и большую часть этой цены обычно составляет не сама зависимость, а неаккуратная подготовка.

// Формулировки: «какая должна быть пирамида тестов?», «сколько интеграционных тестов нужно?», «как ускорить набор?»

Три уровня и что каждый ловит

Модульный тест проверяет одну функцию или класс без внешних зависимостей. Быстрый, точный в указании места поломки, но ничего не знает о том, работают ли части вместе. Интеграционный проверяет связку с настоящей базой, очередью, чужим сервисом - находит именно то, что модульные пропускают: неверный запрос, расхождение схемы, забытую миграцию. Сквозной проходит весь путь пользователя целиком и ловит проблемы сборки и конфигурации.

Чем выше уровень, тем дороже поддержка и тем труднее понять, ЧТО именно сломалось. Отсюда обычная форма пирамиды: много модульных, заметно меньше интеграционных, единицы сквозных - на самые важные сценарии.

// Но пропорции не догма. Если в сервисе почти нет логики, а есть тонкий слой над базой, то модульные тесты будут проверять моки, а не поведение, и полезнее сдвинуть вес к интеграционным. Ориентир один: тест должен ловить настоящие ошибки. Тест, который никогда не краснел на реальной поломке, бесполезен независимо от уровня.

модульный тест
одна функция без внешних зависимостей; быстрый и точный
интеграционный тест
связка с настоящей базой или сервисом; ловит расхождения схемы и запросов
сквозной тест
весь путь пользователя; дорогой и хрупкий, поэтому единицы

Откуда берётся медленный набор

Из моего замера видно, что дорого не то, что кажется. Сама работа с настоящей базой - около миллисекунды на тест. А вот открытие нового соединения - 94 миллисекунды. На пятистах тестах это 47 секунд ровно на подключения.

Второй источник - способ уборки за собой. Откат транзакции стоил 1,14 миллисекунды, полная очистка таблицы - 7,17, то есть в шесть раз дороже. На пятистах тестах разница уже три секунды, а на настоящих схемах с десятками таблиц - десятки секунд.

// Отсюда рецепт быстрого интеграционного набора: базу поднять ОДИН раз на всю сессию, соединение переиспользовать, а изоляцию тестов делать откатом транзакции. Для сравнения: те же тесты на базе в памяти шли по 0,02 миллисекунды - в пятьдесят раз быстрее, но и ловят они меньше, потому что диалект другой.

изоляция откатом
каждый тест в транзакции, которая откатывается; самый дешёвый способ уборки
переиспользование соединения
одно соединение на набор вместо нового на каждый тест

Что проверять на каком уровне

Логику - модульными: расчёты, правила, ветвления, границы. Тут дёшево перебрать все случаи, и именно тут параметризация окупается лучше всего.

Работу с данными - интеграционными: запросы, миграции, ограничения целостности, поведение при параллельных изменениях. Мок базы не проверит ни синтаксис запроса, ни то, что колонку забыли добавить.

// Сквозными - критичные сценарии, где важна именно сборка целиком: вход в систему, оформление заказа, оплата. Их держат единицами и следят за стабильностью: мигающий сквозной тест хуже отсутствующего, потому что его перестают читать и через месяц выключают вместе с настоящими поломками.

уровень проверки
логика - модульно, данные - интеграционно, сценарии - сквозно

Как отвечать: «Сколько нужно интеграционных тестов и как не получить медленный набор?»

Столько, чтобы покрыть работу с данными: запросы, миграции, ограничения целостности - то, что моки не проверяют в принципе. А медленным набор делает обычно не сама база, и это хорошо видно на замерах. Я мерил: работа одного теста с настоящей базой при откате транзакции стоит около миллисекунды, а открытие нового соединения - девяносто четыре, то есть в восемьдесят раз дороже самой работы. На пятистах тестах это минута ровно на подключения. Второе место - способ уборки: откат транзакции стоил миллисекунду, полная очистка таблицы семь. Поэтому рецепт такой: базу поднимаю один раз на всю сессию, соединение переиспользую, каждый тест заворачиваю в транзакцию и откатываю. Тогда интеграционные тесты стоят почти как модульные, и спорить о пропорциях становится не о чем.

Ответ переводит вопрос с пропорций на цену обратной связи и называет настоящий источник медленности с числами. Это ровно то, что делает интеграционные тесты применимыми.

На чём валятся

  • Открывают новое соединение к базе на каждый тест: это дороже самой работы в десятки раз.
  • Чистят данные полной очисткой таблиц вместо отката транзакции.
  • Поднимают контейнер с базой заново на каждый тестовый файл.
  • Пишут модульные тесты на код, где вся суть в запросах к базе, и проверяют моки.
  • Держат десятки сквозных тестов и терпят мигание, пока их не выключат целиком.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.

  1. #test_types1 / 5
    Мок, стаб и фейк — как они различаются?
    A)Стаб отдаёт заготовки, мок проверяет вызовы, фейк — рабочая мини-реализация
    B)Отличаются только библиотекой, которой их создают в тесте
    C)Мок для баз данных, стаб для сети, фейк для файловой системы
    D)Это три названия одного и того же тестового дублёра-заглушки
    показать ответ и разбор
    +A)Стаб отдаёт заготовки, мок проверяет вызовы, фейк — рабочая мини-реализация

    // разбор: Стаб отдаёт заранее заготовленные ответы (не проверяет, как его звали). Мок сверх того проверяет взаимодействие: что метод вызвали с нужными аргументами столько-то раз. Фейк — лёгкая рабочая реализация (in-memory репозиторий вместо БД). Путаница ролей ведёт к тестам, которые проверяют не то.

  2. #test_types2 / 5
    Что обычно стоит мокать в тесте, а что — нет?
    A)Мокать внешние границы (сеть, платёжки), а не свою бизнес-логику
    B)Мокать только собственный код, а внешние сервисы дёргать вживую
    C)Не мокать ничего — мок делает тест недостоверным
    D)Мокать вообще всё вокруг теста, чтобы он был максимально быстрым
    показать ответ и разбор
    +A)Мокать внешние границы (сеть, платёжки), а не свою бизнес-логику

    // разбор: Мокают то, что вне контроля теста и дорого/недетерминированно: внешние API, платёжные шлюзы, отправку писем, время. Собственную бизнес-логику мокать не стоит — иначе тест проверяет заглушки, а не поведение. Хорошая цель мока — граница системы, а не её сердцевина.

  3. #test_types3 / 5
    Тест мокает почти всё, что зовёт функция, и всегда зелёный. Чем это плохо?
    A)Он не запустится в CI без доступа к внешним сервисам вживую
    B)Ничем: чем больше моков, тем изолированнее и надёжнее тест
    C)Он проверяет сами моки, а не реальное поведение кода
    D)Только тем, что такой тест дольше пишется, но результат верный
    показать ответ и разбор
    +C)Он проверяет сами моки, а не реальное поведение кода

    // разбор: Переизбыток моков превращает тест в тавтологию: ты сам задал, что вернут заглушки, и сам это проверил — реальная логика и интеграции не затронуты. Такой тест зелёный даже при сломанном коде и ломается от любого рефакторинга. Симптом — тест повторяет реализацию, а не проверяет наблюдаемый результат.

  4. #test_types4 / 5
    Чем юнит-тест отличается от интеграционного?
    A)Разницы по сути нет, это два названия одного и того же вида автоматических тестов
    B)Юнит проверяет кусок в изоляции (с моками), интеграция — связку реальных компонентов
    C)Юнит-тесты пишут разработчики, а интеграционные — исключительно ручные тестировщики
    D)Юнит-тест медленнее интеграционного, зато охватывает больше кода приложения
    показать ответ и разбор
    +B)Юнит проверяет кусок в изоляции (с моками), интеграция — связку реальных компонентов

    // разбор: Юнит-тест проверяет отдельную единицу логики в изоляции — внешние зависимости (БД, сеть, сервисы) подменяют моками; он быстрый и точечный. Интеграционный проверяет, что несколько реальных компонентов работают вместе (код + настоящая БД, два сервиса) — медленнее, но ловит проблемы стыков, которые юниты с моками пропускают. Нужны оба уровня.

  5. #test_types5 / 5
    Что рекомендует «пирамида тестирования» по их соотношению?
    A)Много быстрых юнитов в основании, меньше интеграционных, совсем мало e2e сверху
    B)Только e2e-тесты: если сквозные проходят, отдельные юниты уже писать не нужно
    C)Больше всего e2e-тестов, ведь они ближе всего к реальному поведению пользователя
    D)Поровну всех видов тестов, чтобы каждый уровень был представлен одинаково полно
    показать ответ и разбор
    +A)Много быстрых юнитов в основании, меньше интеграционных, совсем мало e2e сверху

    // разбор: Пирамида тестирования: широкое основание из быстрых, дешёвых, изолированных юнит-тестов; меньше интеграционных посередине; совсем немного медленных сквозных (e2e) на вершине. Так большинство дефектов ловится быстро и с точной локализацией, а дорогие хрупкие e2e покрывают лишь критичные пользовательские сценарии. Перевёрнутая пирамида (гора e2e) — известный антипаттерн.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.