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

Тестовые данные и контейнеры

Данные и настоящие зависимости в тестах

Поднял настоящий PostgreSQL для тестов: 2363 миллисекунды один раз. Дальше каждый тест обходится в 1,14 миллисекунды, если он живёт в транзакции с откатом. Для сравнения, те же тесты на базе в памяти шли по 0,02 миллисекунды - в пятьдесят раз быстрее, но это уже другая база с другим диалектом.

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

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

Настоящая база против замены

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

Настоящая база в контейнере снимает этот класс расхождений. Мой замер показывает, что цена терпимая: две с небольшим секунды на подъём один раз за сессию и около миллисекунды на тест. Ключевое слово - ОДИН РАЗ: контейнер поднимают на всю сессию, а не на файл и тем более не на тест.

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

база в памяти
быстрая замена с другим диалектом; часть ошибок не ловит
контейнер на сессию
настоящая зависимость поднимается один раз на весь набор

Изоляция тестов друг от друга

Три способа, и они сильно отличаются по цене. Откат транзакции: тест начинается с открытия транзакции, заканчивается откатом, - 1,14 миллисекунды по моему замеру, самый дешёвый. Очистка таблиц после теста - 7,17 миллисекунды, в шесть раз дороже. Пересоздание схемы целиком - секунды, годится только для редких случаев.

Выбор тут не сводится к скорости. Откат не подойдёт, если тестируемый код сам управляет транзакциями или если проверяется поведение при фиксации. Тогда берут очистку, но чистят только затронутые таблицы, а не всё подряд.

// Отдельный вопрос - откуда берутся данные. Готовые дампы из прода удобны и опасны: там персональные данные и там всё меняется. Лучше работают фабрики - функции, создающие объект с разумными значениями по умолчанию и возможностью переопределить нужное поле. Тогда тест говорит только о том, что важно именно ему: «заказ на сумму 100», а остальные поля заполнит фабрика.

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

Другие зависимости

Тот же подход годится и для других зависимостей, а не для одной лишь базы. Очередь, кэш, объектное хранилище - всё это поднимается контейнером на время набора, и тесты работают с настоящим поведением, а не с представлением о нём.

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

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

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

Как отвечать: «База в памяти или настоящая в контейнере?»

Настоящая, если работа с базой - существенная часть кода. Замена в памяти быстрее, я мерил: тест на ней стоит сотые доли миллисекунды против миллисекунды с настоящей базой. Но у неё другой диалект, и она не поймает ни специфичные типы, ни поведение блокировок, ни особенности конфликтов вставки - именно те ошибки, ради которых интеграционные тесты и пишут. Цена настоящей базы оказалась терпимой: подъём контейнера две с небольшим секунды ОДИН раз за сессию, а дальше около миллисекунды на тест при изоляции откатом транзакции. Главное - не поднимать контейнер на каждый файл и не открывать соединение на каждый тест: я мерил, новое соединение стоит девяносто четыре миллисекунды, это в восемьдесят раз дороже самой работы теста.

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

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

  • Тестируют на базе в памяти код, который использует специфику настоящей базы.
  • Поднимают контейнер заново на каждый тестовый файл.
  • Чистят данные полной очисткой вместо отката транзакции.
  • Тащат в тесты дамп прода вместе с персональными данными.
  • Задают контейнеру фиксированный порт и получают конфликты на общей машине.

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

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

  1. #test_data_containers1 / 5
    Как обычно изолируют тесты, работающие с реальной БД, друг от друга?
    A)Полностью удаляют и пересоздают всю базу данных после каждого теста
    B)Полагаются на то, что тесты не пересекутся по данным случайно
    C)Запрещают тестам писать в базу — разрешают только чтение данных
    D)Каждый тест в транзакции, которая откатывается после него
    показать ответ и разбор
    +D)Каждый тест в транзакции, которая откатывается после него

    // разбор: Частый приём: обернуть каждый тест в транзакцию и откатить её в teardown — база возвращается к исходному состоянию, тесты не видят чужих данных и не зависят от порядка. Это быстрее пересоздания схемы. Альтернатива — truncate таблиц между тестами. Общий изменяемый стейт БД без изоляции даёт флаки.

  2. #test_data_containers2 / 5
    Тесты гоняют на SQLite вместо прод-PostgreSQL «для скорости». Риск?
    A)SQLite не поддерживает транзакции, поэтому изоляция тестов сломается
    B)Диалекты расходятся — зелёный тест не гарантирует работу на Postgres
    C)Только скорость: на SQLite тесты медленнее, чем на PostgreSQL
    D)Никакого: SQL стандартизирован, базы ведут себя одинаково
    показать ответ и разбор
    +B)Диалекты расходятся — зелёный тест не гарантирует работу на Postgres

    // разбор: SQLite и PostgreSQL — разные диалекты: отличаются типы (JSONB, массивы, ENUM), поведение оконных функций, ограничений, блокировок, регистр и коллации. Тест, зелёный на SQLite, может падать на проде. Поэтому интеграционные тесты гоняют на той же СУБД — удобно через testcontainers, поднимающий реальный PostgreSQL в контейнере.

  3. #test_data_containers3 / 5
    Почему тесты должны быть независимы друг от друга?
    A)Зависимые тесты запрещены синтаксисом pytest и не запустятся
    B)Иначе порядок и общий стейт делают их хрупкими и флаки
    C)Это формальное требование стиля, на надёжность оно не влияет
    D)Независимость нужна только для запуска тестов в несколько потоков
    показать ответ и разбор
    +B)Иначе порядок и общий стейт делают их хрупкими и флаки

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

  4. #test_data_containers4 / 5
    Тесты гоняют на SQLite, а прод на Postgres. Чем это чревато?
    A)Тесты вообще не запустятся, так как код под Postgres несовместим со SQLite физически
    B)Диалекты расходятся — тесты пропустят баги, воспроизводимые только на Postgres
    C)Только тем, что SQLite медленнее, поэтому тесты будут проходить дольше обычного
    D)Ничем: SQLite и Postgres ведут себя идентично, разница только в скорости выполнения
    показать ответ и разбор
    +B)Диалекты расходятся — тесты пропустят баги, воспроизводимые только на Postgres

    // разбор: SQLite и Postgres различаются диалектом, типами, ограничениями, конкурентностью, поведением транзакций и специфичными фичами (JSONB, оконные функции, upsert). Тесты на SQLite могут быть зелёными, а на проде тот же код падает или ведёт себя иначе. Поэтому интеграционные тесты гоняют на той же СУБД, что и прод — удобнее всего через одноразовый контейнер.

  5. #test_data_containers5 / 5
    Что даёт testcontainers в интеграционных тестах?
    A)Генерирует тестовые данные по схеме базы, не поднимая саму базу данных
    B)Ускоряет тесты, кешируя результаты запросов к базе между отдельными прогонами
    C)Поднимает настоящую зависимость (Postgres, Redis) в Docker на время тестов
    D)Заменяет базу данных лёгким моком в памяти, полностью имитирующим её поведение
    показать ответ и разбор
    +C)Поднимает настоящую зависимость (Postgres, Redis) в Docker на время тестов

    // разбор: testcontainers программно поднимает реальные зависимости в одноразовых Docker-контейнерах на время тестов (Postgres, Redis, Kafka) и гасит их после. Тесты идут против настоящей СУБД той же версии, что и прод — без ручной установки в CI и без расхождений SQLite/Postgres. Цена — нужен Docker и старт контейнера чуть медленнее мока, зато достоверность высокая.

дальше

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

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