Раздел 15. Практические задания
Задания раздела строятся вокруг одного правила: механизм, который не проверяли, обычно не работает. Поэтому почти каждое требует показать не только что проверка проходит, но и что она способна не пройти.
Формулировка «доказать» означает: привести случай, при котором проверка даёт противоположный результат.
Как проверять себя
| Формулировка | Что требуется |
|---|---|
| «показать, что…» | Команда и её фактический вывод |
| «доказать» | Контрольный случай с противоположным результатом |
| «измерить» | Число, полученное командой |
| «обосновать» | Числа или механизм, а не предпочтение |
Задание 1. Стадия test в multi-stage build
Тип: обязательное
Материал: 15.2
Добавить стадию test; сборка должна падать при падении тестов.
Требования:
- Показать, что стадия
testбез ссылок на неё не выполняется при обычной сборке. - Сделать тесты обязательными через
COPY --from=testв финальной стадии. - Проверить в обе стороны: с исправными тестами и со сломанным.
- Показать, что стадия
testнаследует production-образ, а не ветвится от базы. - Доказать это совпадением хеша файла приложения в обоих образах.
- Показать ловушку
RUN pytest | tee logи её исправление черезSHELLсpipefail.
Проверка
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-тесты.
Требования:
- Описать сервисы базы и тестов; проверить конфигурацию через
config --quiet. - Получить код возврата тестов, а не команды Compose.
- Показать, что без
--exit-code-fromупавшие тесты дают код0. - Написать не менее пяти тестов, каждый из которых проверяет то, что подделка воспроизвести не может.
- Убедиться, что база одноразовая: данные не переживают прогон.
Проверка
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 корректной проверкой готовности и измерить выигрыш.
Требования:
- Замерить время прогона с
sleep. - Заменить его на
healthcheckплюсdepends_on: condition: service_healthy. - Замерить время повторно и вычислить разницу.
- Пересчитать выигрыш на число прогонов в неделю.
- Показать, что слабая проверка (
pg_isreadyбез параметров) проходит раньше, чем база готова принять запрос. - Объяснить, почему
sleepплох дважды, а не один раз.
Проверка
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 выполнен опросом обеих проверок в одном цикле на одном экземпляре базы — иначе разрыв недоказуем.
Ориентир для самопроверки
Надёжная проверка готовности делает то же, что приложение:
test: ["CMD-SHELL", "psql -U $$POSTGRES_USER -d $$POSTGRES_DB -c 'SELECT 1' || exit 1"]
pg_isready без -U и -d отвечает на вопрос «отвечает ли сервер вообще», а не «готов ли он для меня».
Задание 4. Изоляция тестов
Тип: обязательное
Материал: 15.3
Два теста, пишущих в одну таблицу, не должны влиять друг на друга.
Требования:
- Выбрать стратегию изоляции и обосновать выбор её ценой и ограничениями.
- Реализовать её фикстурой
pytest. - Доказать изоляцию: написать тот же набор проверок без неё и показать, что он падает.
- Показать, что порядок выполнения тестов перестал влиять на результат.
- Сравнить четыре стратегии по цене одного теста и возможности параллельного запуска.
- Объяснить, почему сравнение только по цене одного теста недостаточно.
Проверка
pytest tests/ -q
pytest tests/ -q -p no:randomly --collect-only | tail -3
pytest tests/test_shared_state.py -q # набор без изоляции — должен падать
Критерий
Набор с изоляцией даёт код 0, тот же набор без неё — ненулевой. Сравнение стратегий учитывает параллельность, а не только время.
Задание 5. Гарантированная очистка
Тип: обязательное
Материал: 15.3
Тестовое окружение должно удаляться даже при падении тестов.
Требования:
- Написать скрипт прогона с
trapнаEXIT,INTиTERM. - Использовать уникальное имя проекта, чтобы параллельные прогоны не конфликтовали.
- Посчитать оставшиеся объекты до и после прогона.
- Проверить очистку при успешном завершении.
- Проверить очистку при падении тестов.
- Проверить очистку при прерывании сигналом
INT. - Объяснить, почему
downбез-vнедостаточно.
Проверка
./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 и сравнить подходы.
Требования:
- Перенести тесты, сохранив изоляцию.
- Использовать правильное сочетание областей действия фикстур.
- Измерить разницу между
scope="session"иscope="function"для container'а. - Показать, что очистка происходит даже при
kill -9процесса тестов. - Назвать требование к CI, которого нет у Compose, и перечислить способы его закрытия с оценкой риска.
- Обосновать выбор подхода для двух разных проектов и получить разные вердикты.
Проверка
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.
Требования:
- Реализовать статические проверки:
USER, формаENTRYPOINT, секреты вENV,EXPOSE, healthcheck. - Реализовать динамические: фактический UID, ответ на запрос, переход healthcheck в
healthy, завершение поSIGTERM. - Проверить файлы без запуска container'а.
- Показать случай, который статическая проверка пропускает, а динамическая ловит.
- Различить «не проверено» и «пройдено» отдельным статусом.
- Выполнять статические проверки первыми и объяснить почему.
Проверка
./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-тестов, переведя тестовую базу в память.
Требования:
- Замерить прогон с томом на диске и включённой сохранностью.
- Перевести данные в
tmpfsи отключитьfsync,synchronous_commit,full_page_writes. - Замерить повторно и вычислить выигрыш.
- Подтвердить, что данные действительно в памяти.
- Подтвердить, что настройки применились.
- Объяснить, почему каждая из настроек недопустима для рабочей базы.
Проверка
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 на подключении к базе. Найти причину.
Требования:
- Назвать пять механизмов, дающих такое расхождение.
- Для каждого предложить проверку, подтверждающую или опровергающую его.
- Воспроизвести хотя бы один механизм.
- Предложить меру, закрывающую наибольшее число механизмов сразу.
- Показать, что мера работает.
- Объяснить, почему увеличение
sleep— не решение, даже если помогает.
Критерий
Пять механизмов названы с проверкой для каждой. Один воспроизведён. Мера проверена, а не заявлена.
Ориентир: пять механизмов
| Механизм | Проверка |
|---|---|
| Гонка при старте: CI-сборщик медленнее, база не успевает | Заменить sleep на condition: service_healthy и повторить |
Слабая проверка готовности: pg_isready прошёл до реальной готовности | Опросить pg_isready и SELECT 1 в одном цикле |
Другое имя хоста: локально localhost, в CI имя сервиса | Сравнить DATABASE_URL в обоих окружениях |
| Ограничение памяти: база не поднимается под лимитом CI | docker inspect --format '{{.State.OOMKilled}}' |
| Остаточное состояние локально: база от прошлого прогона уже была | docker compose down -v перед прогоном и повтор |
Мера, закрывающая три из пяти: ожидание условия вместо времени, причём проверка должна делать то же, что приложение.
Почему увеличение sleep не решение: оно не устраняет гонку, а делает её менее вероятной. Тест остаётся нестабильным — просто падает реже, и связь с причиной теряется окончательно (урок 15.1).
Задание 10. Параллельные прогоны ★
Тип: итоговое
Материал: весь раздел
Организовать параллельный запуск тестовых окружений без конфликта портов и имён.
Требования:
- Обеспечить уникальность имён проектов Compose.
- Обеспечить отсутствие конфликта портов: не публиковать фиксированные порты.
- Обеспечить изоляцию данных между параллельными прогонами.
- Обеспечить изоляцию тестов внутри одного прогона.
- Запустить три прогона одновременно и показать, что все три прошли.
- Показать, что после завершения не осталось ни одного объекта ни от одного прогона.
- Измерить: три прогона параллельно против трёх последовательно.
- Назвать предел параллельности и способ его определить.
Проверка
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 |
| 2 | compose.test.yaml | обяз. | 15.3 |
| 3 | Wait 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 у вас есть практическое подтверждение пяти утверждений раздела:
- Стадия
testбез ссылок на неё не выполняется, и сборка при этом успешна. docker compose upвозвращает0при упавших тестах.sleepплох дважды: долго в обычном случае и мало в плохом.- Изоляция, которую не проверяли контрольным случаем, может не работать.
- Очистка без
trapне выполняется при падении и прерывании.
Задания 6–8 добавляют альтернативный подход и две меры ускорения.
Задание 9 проверяет методику на самом частом расхождении между локальным окружением и CI. Задание 10 собирает всё вместе: параллельность требует, чтобы работали одновременно уникальность имён, случайные порты, изоляция данных и изоляция тестов.
Дальше
Всё это встраивается в автоматический pipeline: раздел 16.
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел: CI/CD →
Главное оглавление