Тестовые данные и контейнеры
Поднял настоящий PostgreSQL для тестов: 2363 миллисекунды один раз. Дальше каждый тест обходится в 1,14 миллисекунды, если он живёт в транзакции с откатом. Для сравнения, те же тесты на базе в памяти шли по 0,02 миллисекунды - в пятьдесят раз быстрее, но это уже другая база с другим диалектом.
Стержень: настоящая зависимость в контейнере стоит секунды на старте и копейки на тест, зато ловит целый класс ошибок, который подмены не видят.
// Формулировки: «как готовить данные для тестов?», «зачем контейнеры в тестах?», «база в памяти или настоящая?»
Настоящая база против замены
Замена базы на встроенную в память соблазнительна: быстро, ничего поднимать не надо. Плата - другой диалект. Специфичные типы, оконные функции, особенности блокировок, поведение при конфликте вставки - всё это может работать иначе или не работать вовсе, и тест зелёный там, где прод красный.
Настоящая база в контейнере снимает этот класс расхождений. Мой замер показывает, что цена терпимая: две с небольшим секунды на подъём один раз за сессию и около миллисекунды на тест. Ключевое слово - ОДИН РАЗ: контейнер поднимают на всю сессию, а не на файл и тем более не на тест.
// Есть и промежуточный вариант, о котором стоит знать: одна общая база для всего набора с изоляцией по транзакциям. Он быстрее, но требует, чтобы тесты не полагались на автономные транзакции внутри, - иначе откат не сработает как ожидалось.
- база в памяти
- быстрая замена с другим диалектом; часть ошибок не ловит
- контейнер на сессию
- настоящая зависимость поднимается один раз на весь набор
Изоляция тестов друг от друга
Три способа, и они сильно отличаются по цене. Откат транзакции: тест начинается с открытия транзакции, заканчивается откатом, - 1,14 миллисекунды по моему замеру, самый дешёвый. Очистка таблиц после теста - 7,17 миллисекунды, в шесть раз дороже. Пересоздание схемы целиком - секунды, годится только для редких случаев.
Выбор тут не сводится к скорости. Откат не подойдёт, если тестируемый код сам управляет транзакциями или если проверяется поведение при фиксации. Тогда берут очистку, но чистят только затронутые таблицы, а не всё подряд.
// Отдельный вопрос - откуда берутся данные. Готовые дампы из прода удобны и опасны: там персональные данные и там всё меняется. Лучше работают фабрики - функции, создающие объект с разумными значениями по умолчанию и возможностью переопределить нужное поле. Тогда тест говорит только о том, что важно именно ему: «заказ на сумму 100», а остальные поля заполнит фабрика.
- изоляция откатом
- тест в транзакции, которая не фиксируется; самый дешёвый способ
- фабрика данных
- функция, создающая объект с разумными умолчаниями и точечными переопределениями
Другие зависимости
Тот же подход годится и для других зависимостей, а не для одной лишь базы. Очередь, кэш, объектное хранилище - всё это поднимается контейнером на время набора, и тесты работают с настоящим поведением, а не с представлением о нём.
Для чужих сервисов, которые не поднять, есть промежуточное решение: локальная заглушка, отвечающая заранее записанными ответами. Она лучше мока тем, что проверяется настоящий сетевой путь: разбор ответа, коды ошибок, предел ожидания.
// И важная деталь про порты. Если тесты запускаются параллельно или на общей машине сборки, фиксированные порты приводят к конфликтам. Поэтому контейнеру дают случайный свободный порт, а тест узнаёт его после запуска. Это же спасает от ситуации, когда на машине разработчика уже занят стандартный порт базы.
- заглушка сервиса
- локальный сервер с заранее записанными ответами; проверяется весь сетевой путь
- случайный порт
- контейнер получает свободный порт; спасает от конфликтов на общей машине
Как отвечать: «База в памяти или настоящая в контейнере?»
Настоящая, если работа с базой - существенная часть кода. Замена в памяти быстрее, я мерил: тест на ней стоит сотые доли миллисекунды против миллисекунды с настоящей базой. Но у неё другой диалект, и она не поймает ни специфичные типы, ни поведение блокировок, ни особенности конфликтов вставки - именно те ошибки, ради которых интеграционные тесты и пишут. Цена настоящей базы оказалась терпимой: подъём контейнера две с небольшим секунды ОДИН раз за сессию, а дальше около миллисекунды на тест при изоляции откатом транзакции. Главное - не поднимать контейнер на каждый файл и не открывать соединение на каждый тест: я мерил, новое соединение стоит девяносто четыре миллисекунды, это в восемьдесят раз дороже самой работы теста.
Ответ сравнивает по тому, какие ошибки каждый вариант ловит, и снимает главный аргумент против настоящей базы измеренной ценой. Замечание про соединение - то, что обычно и делает набор медленным.
На чём валятся
- −Тестируют на базе в памяти код, который использует специфику настоящей базы.
- −Поднимают контейнер заново на каждый тестовый файл.
- −Чистят данные полной очисткой вместо отката транзакции.
- −Тащат в тесты дамп прода вместе с персональными данными.
- −Задают контейнеру фиксированный порт и получают конфликты на общей машине.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.
- Как обычно изолируют тесты, работающие с реальной БД, друг от друга?A)Полностью удаляют и пересоздают всю базу данных после каждого тестаB)Полагаются на то, что тесты не пересекутся по данным случайноC)Запрещают тестам писать в базу — разрешают только чтение данныхD)Каждый тест в транзакции, которая откатывается после него
показать ответ и разбор
+D)Каждый тест в транзакции, которая откатывается после него// разбор: Частый приём: обернуть каждый тест в транзакцию и откатить её в teardown — база возвращается к исходному состоянию, тесты не видят чужих данных и не зависят от порядка. Это быстрее пересоздания схемы. Альтернатива — truncate таблиц между тестами. Общий изменяемый стейт БД без изоляции даёт флаки.
- Тесты гоняют на SQLite вместо прод-PostgreSQL «для скорости». Риск?A)SQLite не поддерживает транзакции, поэтому изоляция тестов сломаетсяB)Диалекты расходятся — зелёный тест не гарантирует работу на PostgresC)Только скорость: на SQLite тесты медленнее, чем на PostgreSQLD)Никакого: SQL стандартизирован, базы ведут себя одинаково
показать ответ и разбор
+B)Диалекты расходятся — зелёный тест не гарантирует работу на Postgres// разбор: SQLite и PostgreSQL — разные диалекты: отличаются типы (JSONB, массивы, ENUM), поведение оконных функций, ограничений, блокировок, регистр и коллации. Тест, зелёный на SQLite, может падать на проде. Поэтому интеграционные тесты гоняют на той же СУБД — удобно через testcontainers, поднимающий реальный PostgreSQL в контейнере.
- Почему тесты должны быть независимы друг от друга?A)Зависимые тесты запрещены синтаксисом pytest и не запустятсяB)Иначе порядок и общий стейт делают их хрупкими и флакиC)Это формальное требование стиля, на надёжность оно не влияетD)Независимость нужна только для запуска тестов в несколько потоков
показать ответ и разбор
+B)Иначе порядок и общий стейт делают их хрупкими и флаки// разбор: Независимый тест не полагается на данные или состояние, оставленные другим: тогда его можно запускать в любом порядке, по одному, параллельно — и результат стабилен. Связанность через общий стейт (глобалы, незачищенная БД) рождает флаки-тесты, которые падают от перестановки или запуска в одиночку.
- Тесты гоняют на SQLite, а прод на Postgres. Чем это чревато?A)Тесты вообще не запустятся, так как код под Postgres несовместим со SQLite физическиB)Диалекты расходятся — тесты пропустят баги, воспроизводимые только на PostgresC)Только тем, что SQLite медленнее, поэтому тесты будут проходить дольше обычногоD)Ничем: SQLite и Postgres ведут себя идентично, разница только в скорости выполнения
показать ответ и разбор
+B)Диалекты расходятся — тесты пропустят баги, воспроизводимые только на Postgres// разбор: SQLite и Postgres различаются диалектом, типами, ограничениями, конкурентностью, поведением транзакций и специфичными фичами (JSONB, оконные функции, upsert). Тесты на SQLite могут быть зелёными, а на проде тот же код падает или ведёт себя иначе. Поэтому интеграционные тесты гоняют на той же СУБД, что и прод — удобнее всего через одноразовый контейнер.
- Что даёт testcontainers в интеграционных тестах?A)Генерирует тестовые данные по схеме базы, не поднимая саму базу данныхB)Ускоряет тесты, кешируя результаты запросов к базе между отдельными прогонамиC)Поднимает настоящую зависимость (Postgres, Redis) в Docker на время тестовD)Заменяет базу данных лёгким моком в памяти, полностью имитирующим её поведение
показать ответ и разбор
+C)Поднимает настоящую зависимость (Postgres, Redis) в Docker на время тестов// разбор: testcontainers программно поднимает реальные зависимости в одноразовых Docker-контейнерах на время тестов (Postgres, Redis, Kafka) и гасит их после. Тесты идут против настоящей СУБД той же версии, что и прод — без ручной установки в CI и без расхождений SQLite/Postgres. Цена — нужен Docker и старт контейнера чуть медленнее мока, зато достоверность высокая.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.