Главная/Testing/Практика

Раздел 15. Практические задания

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

Формулировка «доказать» означает: привести случай, при котором проверка даёт противоположный результат.

Как проверять себя

ФормулировкаЧто требуется
«показать, что…»Команда и её фактический вывод
«доказать»Контрольный случай с противоположным результатом
«измерить»Число, полученное командой
«обосновать»Числа или механизм, а не предпочтение

Задание 1. Стадия test в multi-stage build

Тип: обязательное
Материал: 15.2

Добавить стадию test; сборка должна падать при падении тестов.

Требования:

  1. Показать, что стадия test без ссылок на неё не выполняется при обычной сборке.
  2. Сделать тесты обязательными через COPY --from=test в финальной стадии.
  3. Проверить в обе стороны: с исправными тестами и со сломанным.
  4. Показать, что стадия test наследует production-образ, а не ветвится от базы.
  5. Доказать это совпадением хеша файла приложения в обоих образах.
  6. Показать ловушку RUN pytest | tee log и её исправление через SHELL с pipefail.

Проверка

bash
docker build --no-cache . 2>&1 | grep -c 'passed'
docker build --no-cache . ; echo "код: $?"
docker run --rm --entrypoint sh ОБРАЗ -c 'sha256sum /app/src/main.py'

Критерий

Первая команда находит строку о пройденных тестах. Со сломанным тестом сборка даёт ненулевой код и образ не создаётся. Хеши файла в production и test совпадают.

Ориентир для самопроверки

Признак того, что задание выполнено формально: тесты в сборке есть, но никто не проверял, останавливают ли они её. Сломайте один тест — если образ всё равно собрался, механизм не работает.

Три способа потерять код возврата: || true, ; echo, | tee. Последний встречается чаще всего и выглядит безобидно.


Задание 2. compose.test.yaml с одноразовой базой

Тип: обязательное
Материал: 15.3

Написать compose.test.yaml с PostgreSQL и запустить integration-тесты.

Требования:

  1. Описать сервисы базы и тестов; проверить конфигурацию через config --quiet.
  2. Получить код возврата тестов, а не команды Compose.
  3. Показать, что без --exit-code-from упавшие тесты дают код 0.
  4. Написать не менее пяти тестов, каждый из которых проверяет то, что подделка воспроизвести не может.
  5. Убедиться, что база одноразовая: данные не переживают прогон.

Проверка

bash
docker compose -f compose.test.yaml config --quiet
docker compose -f compose.test.yaml up --abort-on-container-exit --exit-code-from tests; echo "код: $?"
docker compose -f compose.test.yaml down -v

Критерий

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


Задание 3. Wait strategy вместо sleep

Тип: обязательное
Материал: 15.3

Заменить sleep 10 корректной проверкой готовности и измерить выигрыш.

Требования:

  1. Замерить время прогона с sleep.
  2. Заменить его на healthcheck плюс depends_on: condition: service_healthy.
  3. Замерить время повторно и вычислить разницу.
  4. Пересчитать выигрыш на число прогонов в неделю.
  5. Показать, что слабая проверка (pg_isready без параметров) проходит раньше, чем база готова принять запрос.
  6. Объяснить, почему sleep плох дважды, а не один раз.

Проверка

bash
time docker compose -f compose.sleep.yaml up --abort-on-container-exit --exit-code-from tests
time docker compose -f compose.test.yaml up --abort-on-container-exit --exit-code-from tests

Критерий

Разница измерена, а не оценена. Пункт 5 выполнен опросом обеих проверок в одном цикле на одном экземпляре базы — иначе разрыв недоказуем.

Ориентир для самопроверки

Надёжная проверка готовности делает то же, что приложение:

yaml
test: ["CMD-SHELL", "psql -U $$POSTGRES_USER -d $$POSTGRES_DB -c 'SELECT 1' || exit 1"]

pg_isready без -U и -d отвечает на вопрос «отвечает ли сервер вообще», а не «готов ли он для меня».


Задание 4. Изоляция тестов

Тип: обязательное
Материал: 15.3

Два теста, пишущих в одну таблицу, не должны влиять друг на друга.

Требования:

  1. Выбрать стратегию изоляции и обосновать выбор её ценой и ограничениями.
  2. Реализовать её фикстурой pytest.
  3. Доказать изоляцию: написать тот же набор проверок без неё и показать, что он падает.
  4. Показать, что порядок выполнения тестов перестал влиять на результат.
  5. Сравнить четыре стратегии по цене одного теста и возможности параллельного запуска.
  6. Объяснить, почему сравнение только по цене одного теста недостаточно.

Проверка

bash
pytest tests/ -q
pytest tests/ -q -p no:randomly --collect-only | tail -3
pytest tests/test_shared_state.py -q   # набор без изоляции — должен падать

Критерий

Набор с изоляцией даёт код 0, тот же набор без неё — ненулевой. Сравнение стратегий учитывает параллельность, а не только время.


Задание 5. Гарантированная очистка

Тип: обязательное
Материал: 15.3

Тестовое окружение должно удаляться даже при падении тестов.

Требования:

  1. Написать скрипт прогона с trap на EXIT, INT и TERM.
  2. Использовать уникальное имя проекта, чтобы параллельные прогоны не конфликтовали.
  3. Посчитать оставшиеся объекты до и после прогона.
  4. Проверить очистку при успешном завершении.
  5. Проверить очистку при падении тестов.
  6. Проверить очистку при прерывании сигналом INT.
  7. Объяснить, почему down без -v недостаточно.

Проверка

bash
./run-tests.sh; echo "код: $?"
docker ps -a --filter "name=ИМЯ_ПРОЕКТА" -q | wc -l
docker volume ls -q --filter "name=ИМЯ_ПРОЕКТА" | wc -l

Критерий

После всех трёх сценариев остаётся ноль container'ов и ноль томов. Счёт «до» приведён — иначе ноль после ничего не доказывает.

Ориентир для самопроверки

Проверить прерывание: запустить скрипт в фоне, подождать несколько секунд, послать kill -INT.

Код возврата при этом будет 130 — это 128 + 2, завершение по SIGINT (урок 13.3).


Задание 6. Тот же набор на Testcontainers

Тип: дополнительное
Материал: 15.4

Переписать набор тестов из задания 2 на Testcontainers и сравнить подходы.

Требования:

  1. Перенести тесты, сохранив изоляцию.
  2. Использовать правильное сочетание областей действия фикстур.
  3. Измерить разницу между scope="session" и scope="function" для container'а.
  4. Показать, что очистка происходит даже при kill -9 процесса тестов.
  5. Назвать требование к CI, которого нет у Compose, и перечислить способы его закрытия с оценкой риска.
  6. Обосновать выбор подхода для двух разных проектов и получить разные вердикты.

Проверка

bash
pytest tests/ -q --durations=5
docker ps --filter 'ancestor=testcontainers/ryuk' --format '{{.Names}}'
docker ps -a --filter 'label=org.testcontainers=true' | wc -l

Критерий

Оба варианта области действия дают одинаковый результат тестов и разное время. Пункт 4 выполнен фактическим убийством процесса, а не рассуждением. Пункт 6 даёт разные вердикты — иначе критерии не различают ситуации.

Ориентир для самопроверки

Правильное сочетание: container — scope="session", изоляция схемой — scope="function".

Ошибка «container на каждый тест ради изоляции» даёт ту же изоляцию, поднимая PostgreSQL сотню раз вместо одного.

trap при kill -9 не срабатывает: SIGKILL нельзя перехватить. Ryuk работает, потому что это отдельный процесс, следящий за соединением.


Задание 7. Проверки собранного образа

Тип: дополнительное
Материал: 15.5

Написать валидацию: UID процесса, отсутствие dev-зависимостей, наличие healthcheck.

Требования:

  1. Реализовать статические проверки: USER, форма ENTRYPOINT, секреты в ENV, EXPOSE, healthcheck.
  2. Реализовать динамические: фактический UID, ответ на запрос, переход healthcheck в healthy, завершение по SIGTERM.
  3. Проверить файлы без запуска container'а.
  4. Показать случай, который статическая проверка пропускает, а динамическая ловит.
  5. Различить «не проверено» и «пройдено» отдельным статусом.
  6. Выполнять статические проверки первыми и объяснить почему.

Проверка

bash
./validate.sh ОБРАЗ; echo "код: $?"
docker inspect ОБРАЗ --format '{{.Config.User}} {{json .Config.Entrypoint}}'
docker run --rm --entrypoint id ОБРАЗ -u

Критерий

Валидация даёт разные коды на правильном и сломанном образе. Пункт 4 требует третьего образа — например, с USER app без создания пользователя.


Задание 8. Ускорение через tmpfs

Тип: дополнительное
Материал: 15.3

Ускорить прогон integration-тестов, переведя тестовую базу в память.

Требования:

  1. Замерить прогон с томом на диске и включённой сохранностью.
  2. Перевести данные в tmpfs и отключить fsync, synchronous_commit, full_page_writes.
  3. Замерить повторно и вычислить выигрыш.
  4. Подтвердить, что данные действительно в памяти.
  5. Подтвердить, что настройки применились.
  6. Объяснить, почему каждая из настроек недопустима для рабочей базы.

Проверка

bash
docker compose exec db df -h /var/lib/postgresql/data
docker compose exec db psql -U test -d testdb -tAc "SHOW fsync"
docker compose exec db psql -U test -d testdb -tAc "SHOW synchronous_commit"

Критерий

Первая команда показывает тип tmpfs, остальные — off. Выигрыш измерен на одинаковом наборе тестов.


Задание 9. Диагностика: локально проходят, в CI падают

Тип: диагностическое
Материал: весь раздел

Тесты проходят локально и падают в CI на подключении к базе. Найти причину.

Требования:

  1. Назвать пять механизмов, дающих такое расхождение.
  2. Для каждого предложить проверку, подтверждающую или опровергающую его.
  3. Воспроизвести хотя бы один механизм.
  4. Предложить меру, закрывающую наибольшее число механизмов сразу.
  5. Показать, что мера работает.
  6. Объяснить, почему увеличение sleep — не решение, даже если помогает.

Критерий

Пять механизмов названы с проверкой для каждой. Один воспроизведён. Мера проверена, а не заявлена.

Ориентир: пять механизмов
МеханизмПроверка
Гонка при старте: CI-сборщик медленнее, база не успеваетЗаменить sleep на condition: service_healthy и повторить
Слабая проверка готовности: pg_isready прошёл до реальной готовностиОпросить pg_isready и SELECT 1 в одном цикле
Другое имя хоста: локально localhost, в CI имя сервисаСравнить DATABASE_URL в обоих окружениях
Ограничение памяти: база не поднимается под лимитом CIdocker inspect --format '{{.State.OOMKilled}}'
Остаточное состояние локально: база от прошлого прогона уже былаdocker compose down -v перед прогоном и повтор

Мера, закрывающая три из пяти: ожидание условия вместо времени, причём проверка должна делать то же, что приложение.

Почему увеличение sleep не решение: оно не устраняет гонку, а делает её менее вероятной. Тест остаётся нестабильным — просто падает реже, и связь с причиной теряется окончательно (урок 15.1).


Задание 10. Параллельные прогоны ★

Тип: итоговое
Материал: весь раздел

Организовать параллельный запуск тестовых окружений без конфликта портов и имён.

Требования:

  1. Обеспечить уникальность имён проектов Compose.
  2. Обеспечить отсутствие конфликта портов: не публиковать фиксированные порты.
  3. Обеспечить изоляцию данных между параллельными прогонами.
  4. Обеспечить изоляцию тестов внутри одного прогона.
  5. Запустить три прогона одновременно и показать, что все три прошли.
  6. Показать, что после завершения не осталось ни одного объекта ни от одного прогона.
  7. Измерить: три прогона параллельно против трёх последовательно.
  8. Назвать предел параллельности и способ его определить.

Проверка

bash
for i in 1 2 3; do ./run-tests.sh & done; wait
docker ps -a --filter 'name=tests-' -q | wc -l
docker volume ls -q --filter 'name=tests-' | wc -l
docker network ls --filter 'name=tests-' -q | wc -l

Критерий

Три прогона завершились успешно и одновременно. После завершения все три команды дают ноль. Выигрыш от параллельности измерен.

Ориентир: четыре уровня изоляции
УровеньМеханизм
Между прогонами: имена-p "tests-$$-$(date +%s)"
Между прогонами: портыНе публиковать вовсе либо -p 0:5432
Между прогонами: данныеСвой том или tmpfs на проект
Внутри прогона: тестыСхема на тест

Предел параллельности определяется памятью: каждый прогон поднимает свой экземпляр базы. Измерить — запуская по возрастанию числа прогонов и наблюдая за docker stats и признаком OOM (урок 13.3).

Признак хорошего решения: команда for i in $(seq 1 5); do ./run-tests.sh & done; wait завершается без единого конфликта и без остатков.


Сводная таблица

ЗаданиеТипОсновной материал
1Стадия test в сборкеобяз.15.2
2compose.test.yamlобяз.15.3
3Wait strategy вместо sleepобяз.15.3
4Изоляция тестовобяз.15.3
5Гарантированная очисткаобяз.15.3
6Тот же набор на Testcontainersдоп.15.4
7Проверки собранного образадоп.15.5
8Ускорение через tmpfsдоп.15.3
9Локально проходят, в CI падаютдиаг.весь раздел
10Параллельные прогонывесь раздел

Что должно получиться

После заданий 1–5 у вас есть практическое подтверждение пяти утверждений раздела:

  1. Стадия test без ссылок на неё не выполняется, и сборка при этом успешна.
  2. docker compose up возвращает 0 при упавших тестах.
  3. sleep плох дважды: долго в обычном случае и мало в плохом.
  4. Изоляция, которую не проверяли контрольным случаем, может не работать.
  5. Очистка без trap не выполняется при падении и прерывании.

Задания 6–8 добавляют альтернативный подход и две меры ускорения.

Задание 9 проверяет методику на самом частом расхождении между локальным окружением и CI. Задание 10 собирает всё вместе: параллельность требует, чтобы работали одновременно уникальность имён, случайные порты, изоляция данных и изоляция тестов.

Дальше

Всё это встраивается в автоматический pipeline: раздел 16.

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий раздел: CI/CD →
Главное оглавление

Markdown на GitHub ↗