Перейти к содержимому

«У нас coverage 80%, с тестами всё нормально» — каждый раз, когда я это слышу, у меня появляется один встречный вопрос. Покрытие чего именно, и сколько багов оно поймало? Покрытие — диагностика, а не цель. Этот лист про архитектуру портфеля тестов: какие слои (unit, integration, contract, e2e), какой ценой они поддерживаются и какие регрессии ловят. Третий лист под L1 Programming / Scripting, сосед к Programming Languages и CI/CD.

Главный навык на уровне L4 — проектировать портфель тестов под конкретную систему. Пирамида (много unit, меньше integration, совсем мало e2e) — ориентир для большинства систем, но не догма. Для распределённых систем, по моим наблюдениям, лучше работает honeycomb с упором на integration и contract. Для backend без сложного UI — trophy (Kent C. Dodds). Ошибка в выборе формы стоит дорого и оплачивается годами: медленный и хрупкий набор накапливается незаметно.

L3

  • Пишет unit-тесты для своего кода: проверяет публичный интерфейс, а не внутренности; понимает разницу между unit, integration и e2e.
  • Применяет табличные тесты и тесты на свойствах, чтобы покрыть пространство входов; не пишет десяток однотипных копий с разными константами.

L4

  • Проектирует портфель тестов для сервиса: какие слои нужны, какие зависимости берутся настоящими, а какие подменяются, где живут интеграционные тесты — в контейнерах рядом с прогоном, в общем staging или в одноразовом окружении.
  • Применяет contract testing между сервисами (Pact / Spring Cloud Contract / Hoverfly) — контракт задаёт потребитель, а проверяют его в CI обе стороны.
  • Относится к нестабильным тестам как к эксплуатационной задаче: измеряет долю миганий, отправляет такие тесты в карантин и разбирает причину, а не отчитывается «перезапуском закрыли».

L5

  • Проектирует стратегию тестовых данных: фикстуры, фабрики, билдеры или эталонные наборы; постоянная тестовая база или одноразовая на каждый тест; отдельно — что делать с персональными данными.
  • Внедряет мутационное тестирование как метрику качества набора тестов (Pitest / Stryker / Cosmic Ray / go-mutesting).
  • Проектирует нефункциональные проверки — производительность, нагрузка, стресс и длительный прогон — как отдельные категории со своими окружениями, точками отсчёта и критериями приёмки.

L6+

  • Внедряет стандарты уровня организации: минимальная планка по покрытию и мутационному счёту в зависимости от критичности сервиса, рубежи в CI, общая тестовая инфраструктура, владелец у каждого набора и регулярная чистка.
  • Kent Beck — Test-Driven Development: By Example (Addison-Wesley, 2002). Канон TDD, но шире — про дисциплину малых шагов. Читать стоит, даже если строгий TDD вы не практикуете.
  • Lisa Crispin, Janet Gregory — Agile Testing (Addison-Wesley, 2008). Квадранты тестирования, пирамида, тестирование как дело всей команды.
  • Roy Osherove — The Art of Unit Testing (Manning, 3rd ed. 2024). Прикладная книга про паттерны модульных тестов, разницу между подменами разного рода и признаки плохих тестов. Третье издание переписано под современный стек.
  • Vladimir Khorikov — Unit Testing Principles, Practices, and Patterns (Manning, 2020). Две школы (лондонская и детройтская), что вообще считать модулем и где проходит граница с интеграционными тестами. Более глубокий заход после Osherove.
  • Mike Cohn — The Forgotten Layer of the Test Automation Pyramid (2009). Оригинал идеи пирамиды тестов.
  • Martin Fowler — Test Pyramid и On the Diverse And Fantastical Shapes of Testing. Фаулер возвращается к пирамиде двенадцать лет спустя и разбирает другие формы под разные системы. Полезно ради понимания, что пирамида — ориентир, а не догма.
  • Google Testing Blog — Just Say No to More End-to-End Tests (2015). Главный кейс листа — см. ниже.
  • Martin Fowler — Eradicating Non-Determinism in Tests. Канонический разбор причин миганий и способов их устранить. Читать до того, как отправлять тесты в карантин.
  • John Micco — Flaky Tests at Google and How We Mitigate Them (Google Testing Blog, 2016). Масштаб проблемы в цифрах: 1.5% всех прогонов заканчиваются flake’ом, почти 16% тестов хоть раз мигали, 84% переходов pass → fail оказываются flake’ом, а не регрессией. Держу под рукой как аргумент для тех, кто считает мигающий тест мелочью.
  • Pact docs intro. Объяснение того, как контракт задаётся со стороны потребителя.
  • Stryker — An introduction to mutation testing. Мутационный счёт как метрика, дополняющая покрытие.
  • Spotify Labs — Testing of Microservices. Соты вместо пирамиды: как перебалансировать портфель под микросервисы.
  • Andrew Trenk, Adam Bender — Software Engineering at Google: Testing Overview (O’Reilly, 2020). Как в Google смотрят на тестирование на своём масштабе.
  • Прогон unit и integration: JUnit (Java/Kotlin), pytest (Python), Jest / Vitest (JS/TS), go test + testify (Go), cargo test (Rust).
  • Тесты на свойствах: Hypothesis (Python), proptest (Rust), jqwik (Java), fast-check (JS/TS).
  • Подмены зависимостей: Mockito (Java), unittest.mock (Python), WireMock (HTTP), gomock (Go).
  • Контейнеры под интеграционные тесты: Testcontainers — одноразовые база, Kafka или Redis в Docker на каждый прогон. По моим наблюдениям, стандарт во всех языках с приличной поддержкой Docker. LocalStack поднимает сервисы AWS локально.
  • Контрактные тесты: Pact (контракт со стороны потребителя, с брокером), Spring Cloud Contract, Hoverfly.
  • Сквозные и браузерные: Playwright — к 2026 выбор по умолчанию для веба; Cypress — альтернатива; Selenium — наследие, но всё ещё нужный запасной вариант.
  • Покрытие: Coverage.py, JaCoCo, Istanbul/nyc, go test -coverprofile.
  • Мутационное тестирование: PIT (Pitest) (JVM), Stryker (JS / .NET / Scala), mutmut (Python).
  • Нагрузка и производительность: k6 — по моим наблюдениям, нынешний выбор по умолчанию; Locust (Python), Gatling, Vegeta.
  • Учёт миганий: BuildPulse, Trunk Flaky Tests, Datadog CI Visibility. Без инструмента долю миганий никто не считает, и это остаётся слепым пятном.

Канонический публичный аргумент против перекоса в сквозные тесты — Google Testing Blog «Just Say No to More End-to-End Tests» (2015). В нём Google публично объяснил, почему даже их огромная инфраструктура не справляется с поддержкой большого набора e2e: тесты постоянно мигают, обратная связь занимает часы вместо минут, а поддержка стоит непропорционально дорого. Аргумент простой. Если кто-то в команде предлагает «давайте больше e2e, они же ближе к пользователю» — отправляйте читать эту статью первым делом. Пирамида — много дешёвых unit, меньше дорогих integration, совсем мало e2e — выросла именно из этого опыта.

Отсюда первое правило: покрытие — диагностика, а не цель. Стоит объявить «80% обязательно», и закон Гудхарта срабатывает мгновенно: появляются тесты без единой проверки и тесты на геттеры. Цифра растёт, сигнала нет. Полезно покрытие ровно в обратную сторону: он неплохо показывает, что не покрыто вообще, и позволяет сравнить между собой модули, которые писали разные люди в разное время и с разным отношением к тестам. Честнее меряет мутационный счёт, но и стоит он дороже: каждый прогон умножается на число мутаций.

Контрактные тесты я считаю обязательным слоем в распределённых системах, а не задачей «когда-нибудь потом». Модульные тесты проверяют сервис изолированно, сквозные медленные и нестабильные, а потребитель ломается о несовместимое изменение у поставщика только при совместной выкатке — то есть постфактум и обычно в проде. Pact и Spring Cloud Contract переносят эту проверку в CI каждой стороны отдельно. Внедрение стоит примерно час на сервис, а взамен из жизни уходит целый класс интеграционных багов.

Окружение для тестов — эфемерное на каждый прогон, не общий staging. Общий staging копит мусор в тестовых данных, а тесты в нём сталкиваются друг с другом: данные грязные после соседнего прогона, порты заняты, гонки возникают на ровном месте и воспроизводятся раз в десять запусков. Отдельное окружение на PR (Docker Compose, Testcontainers, namespace в k8s на ветку) даёт изоляцию. При нормальных инструментах стоит это вменяемых денег.

Пирамида — отправная точка, а не догма: форма выбирается под систему. Перевёрнутая пирамида, где сквозных тестов много, а модульных мало, даёт обратную связь через часы, хрупкость и дорогую поддержку. Базовая пирамида подходит большинству. Для распределённых систем, по моим наблюдениям, лучше ложатся соты — упор на интеграционные и контрактные тесты, туда, где эти баги и появляются. Для бэкенда без сложного интерфейса — трофей (Kent C. Dodds). Главное, чтобы форма была выбрана осознанно, а не по принципу «у всех пирамида, и у нас пирамида».

Долю миганий меряют, нестабильные тесты изолируют. «Перезапуск помог, идём дальше» — короткая дорога к тому, что красному CI перестают верить вообще: инженер видит красное, жмёт перезапуск не глядя, а настоящая регрессия проезжает мимо. Порог тут жёстче, чем кажется на глаз. В Software Engineering at Google он назван прямо: по опыту авторов набор начинает терять ценность уже на подходе к одному проценту миганий, а собственный уровень Google держится около 0.15%. Лечится это метрикой на каждый тест, дашбордом и еженедельным разбором. Дальше жизненный цикл: карантин (тест не блокирует merge) → разбор причины → починить или удалить. Trunk, BuildPulse и Datadog CI Visibility делают мигания видимыми, без них это слепое пятно.

Тестовые данные — отдельное решение, а не «выгрузим из прода». Я регулярно вижу два антипаттерна. Первый — дамп прода в staging без анонимизации: чистый провал с персональными данными, о котором юристы узнают последними и очень некстати. Второй — руками написанные фикстуры в каждом тесте, которые расползаются, дублируются и превращают поддержку в кошмар. Дальше выбор по вкусу. Фабрики дают собираемые из кусочков данные, билдеры — читаемость, эталонные наборы — точку отсчёта для регрессий, одноразовая база через Testcontainers — изоляцию.

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

  • CI/CD — тесты живут в конвейере, и он остаётся быстрым только без мигающих тестов. Без стратегии зелёный CI даёт ложную уверенность. Сосед под тем же L1.
  • Programming Languages — у каждого языка свои инструменты: табличные тесты в go test, фикстуры в pytest, аннотации в JUnit. Сосед под тем же L1.
  • Progressive Delivery — канареечная выкатка с проверкой здоровья — это тест уже на живом трафике. Граница простая: тесты до выкатки, канарейка после.
  • Chaos Engineering — chaos ставит гипотезы на живой системе, а нагрузочные тесты создают заранее известное давление в staging. Дополняют друг друга, но не заменяют.
  • Resilience Patterns — тестируемость — свойство архитектуры: идемпотентность, внедрение зависимостей, побочные эффекты, вынесенные на границы. Устойчивый дизайн и тестируемый обычно совпадают.
  • SLO Engineering — нагрузочные тесты дают точки отсчёта для SLI и SLO. Без них целевые числа — догадка.
  • Toil Tracking — ручной прогон тестов — это toil; автоматизация его убирает.
  • Vulnerability Management — проверки безопасности (SAST, DAST, фаззинг) — отдельная категория в портфеле.
  • Performance & Profiling — нагрузочные тесты меряют систему под нагрузкой, а профилирование продолжает эти измерения уже после выкатки. Соседние листы.

Четыре темы я держу в уме, но пока не написал. Build reproducibility и hermetic builds (TBD) — детерминированная сборка в духе Bazel; это либо отдельный лист под Programming / Scripting, либо подраздел Supply Chain Security, и я не решил, куда честнее. Фаззинг (TBD) — libFuzzer, AFL, jazzer, OSS-Fuzz, встроенный фаззер в Go: пахнет безопасностью, а по технике это те же тесты на свойствах, только с генерируемым пространством входов. TDD (TBD) — граница с этим листом проходит по тому, что TDD задаёт порядок работы, а Test Strategy — архитектуру портфеля. И теневой трафик (TBD): запись боевых запросов и их повтор против новой версии в staging — штука, которая одновременно про chaos, нагрузку, регрессии и контракты.