Сколько компонентных тестов нужно сервису. Логический вывод
«Тестирование программ может служить для доказательства наличия ошибок, но никогда не докажет их отсутствия!»
— Edsger W. Dijkstra
В предыдущих статьях мы прошли путь:
- «Модульность программы» — модуль как чёрный ящик с одним входом и одним выходом.
- «Правильность программы» — правильность достигается проектированием. Контроль диапазонов значений, защищённое программирование, формальное доказательство.
- «README — это продукт» — документация как продукт, у которого есть потребители.
Складываем три кирпича — и приходим к вопросу, на который индустрия не даёт внятного ответа: сколько компонентных тестов нужно сервису?
Ответ выводится логически. И он мал.
В этой главе рассмотрим:
- Зачем вообще нужен тест
- Тест читают трое
- Что описывает спецификация компонента
- Почему «протестировать всё» невозможно
- Сколько тестов нужно. Формула
- Что не нужно проверять компонентным тестом
- Аналогия с электроникой
- Что меняется для вайб-кодера
Зачем вообще нужен тест
Вайб-кодер, который пришёл в код через ИИ, часто слышит: «тесты ловят баги». Это неправда. И неправда давно.
Дейкстра в 1972 году доказал: тестирование показывает наличие ошибок, но никогда не доказывает их отсутствия. Прогнать сто тестов и увидеть «все зелёные» — не означает, что программа работает. Означает только, что эти сто конкретных случаев работают.
Так зачем тогда писать тесты вообще?
Тест — это документация поведения программы. Только не на русском языке, а в форме, которую может прочитать машина. Это исполняемая спецификация: что сервис обещает делать на штатном пути и что он обещает делать, когда что-то идёт не так.
Эту мысль развил Гойко Аджич в книге Specification by Example: один документ читают аналитик, разработчик и машина. Документ описывает, как должен работать сервис. Машина периодически прогоняет документ через настоящий код и говорит — соответствует поведение спецификации или нет.
Если сценарий «прошёл» — это не «бага нет». Это «поведение совпало со спецификацией». Если хотите больше уверенности — нужно лучше проектировать программу, а не писать больше тестов.
Тест читают трое
В 2026 году у тестов три читателя:
- Разработчик-человек. Открывает файл, чтобы понять, что сервис умеет.
- Вайб-кодер. Тот же разработчик, только через ИИ. Открывает файл и просит ИИ-агента изменить или дополнить поведение. Агент читает тест, чтобы понять контракт.
- Машина. ИИ-агент, который автоматически генерирует код, или CI, который прогоняет сценарии при каждом коммите.
Все трое читают одно и то же. Поэтому тест должен быть написан на языке предметной области, а не на языке кода и моков.
Формат, который подходит всем троим, существует. Это Gherkin — синтаксис из BDD, в котором сценарий описан как «дано — когда — тогда».
Feature: Регистрация клиента
Scenario: Успешная регистрация совершеннолетнего клиента
Given сервис регистрации запущен
And БД доступна
And брокер сообщений доступен
When клиент отправляет POST /clients с валидными данными
Then ответ 201 Created с идентификатором клиента
And запись клиента сохранена в БД
And событие client_registered опубликовано в брокер
Это не «техническая документация для разработчиков». Это нормальный русский текст с минимальной структурой. Аналитик читает и понимает, какое поведение задумано. Разработчик читает и понимает, что нужно реализовать. ИИ-агент читает и понимает контракт. Машина прогоняет — и проверяет, что код этому контракту соответствует.
Один артефакт — четыре читателя. Поэтому Gherkin.
Что описывает спецификация компонента
Спецификация компонента — это описание сервиса как чёрного ящика. Снаружи у сервиса есть API. Внутри — модули, которые мы спроектировали правильно (это тема предыдущих статей).
Спецификация описывает только то, что видно снаружи через API:
-
Штатное поведение. Что сервис делает, когда всё хорошо. Один сценарий: валидный запрос, корректный ответ, ожидаемые эффекты на интеграциях (запись в БД, событие в брокер).
-
Поведение при отказе внешних связей. Что сервис делает, когда падает БД, не отвечает брокер, недоступен внешний сервис. По сценарию на каждую такую связь.
Всё. Больше в спецификации компонента нет ничего.
Может показаться, что этого мало. Что про валидацию полей? Про проверку возраста? Про обработку битого JSON? Про разные комбинации входных данных?
Это всё проверяется — но не на уровне компонента, а юнит-тестами внутри модулей. Об этом дальше.
Сейчас важно понять: спецификация компонента описывает контракт сервиса с внешним миром. Не его внутреннее устройство.
Почему «протестировать всё» невозможно
У вайб-кодера часто возникает интуиция: «давай сгенерируем побольше тестов, на всякий случай». ИИ это умеет — он может за минуту сгенерировать сотню сценариев. Кажется, что чем больше — тем лучше.
Это интуиция, которую нужно сломать числами.
Пример первый. Возраст клиента
Допустим, в нашем сервисе регистрации поле age хранится как int8 — целое число от 0 до 127. Бизнес-правило: регистрировать только совершеннолетних, то есть 18 и старше.
Сколько сценариев нужно, чтобы «проверить все возможные значения возраста»?
128 сценариев. Каждый — отдельный тест: «при возрасте N сервис ведёт себя так-то». Уже многовато для одного поля.
Пример второй. ФИО клиента
ФИО — строка до 100 символов кириллицы. Условно, 33 буквы (без разделения регистров для простоты).
Сколько вариантов значений?
Семёрка и сто пятьдесят один ноль.
Сколько это машинного времени. Допустим, машина очень быстрая — миллиард сценариев в секунду (10⁹). На пальцах:
- комбинаций ≈ 7 × 10¹⁵¹;
- делим на 10⁹ операций в секунду → ≈ 7 × 10¹⁴² секунд;
- в году ≈ 3 × 10⁷ секунд → делим → ≈ 2 × 10¹³⁵ лет.
Сравним с возрастом Вселенной — 13,8 миллиарда лет (1,4 × 10¹⁰ лет):
То есть на исчерпывающую проверку одного поля ФИО потребуется в 10¹²⁵ раз больше времени, чем существует Вселенная. И это при скорости в миллиард тестов в секунду — реальный тест на порядки медленнее.
На одно поле.
А в API сервиса регистрации полей не одно — есть ещё email, дата рождения, адрес. И каждое поле — независимое измерение. Полные комбинации значений всех полей — это произведение комбинаций каждого. Никакая машина никогда не успеет «проверить всё».
Возражение: но мы же комбинируем слова, а не случайные буквы
Если заменить математический хаос на реальные данные, картина меняется радикально. Мы переходим от бесконечного перебора символов к работе со словарями.
Исходные данные для России. По статистическим реестрам и архивам ЗАГС количество уникальных компонентов ФИО примерно такое:
- фамилий — около 1 000 000 (из них активных, встречающихся чаще четырёх раз, — около 50 000);
- имён — около 100 000;
- отчеств — около 20 000.
Расчёт комбинаций. Если просто перебирать любое имя с любой фамилией и любым отчеством (даже без учёта согласования по полу):
Сколько это машинного времени. При той же скорости 10⁹ операций в секунду:
- секунды: 2 × 10¹⁵ / 10⁹ = 2 × 10⁶;
- часы: 2 × 10⁶ / 3600 ≈ 556;
- дни: 556 / 24 ≈ 23.
Около трёх недель на одной быстрой машине.
Итог сравнения.
| Подход | Время прогона |
|---|---|
| Перебор символов (33¹⁰⁰ ≈ 7 × 10¹⁵¹) | 2 × 10¹³⁵ лет (бесконечность) |
| Перебор реальных слов (2 × 10¹⁵) | около трёх недель |
| Топ-1000 фамилий × Топ-1000 имён | ~0,001 секунды |
Что это значит для практики. Даже «реальный» перебор реалистичных значений выходит далеко за рамки прогона тестов в CI. Любое сокращение — Топ-1000, Топ-100, частотные распределения — превращает исчерпывающий тест в случайную выборку: Топ-1000 фамилий покрывает 13–30% населения, и на оставшиеся 70–87% корректность не доказана.
Аргумент Дейкстры и Вирта остаётся в силе: исчерпывающее экспериментальное тестирование бессмысленно. Сужение до словарей просто понижает потолок «бесконечности» до «недель», но выборочный тест не превращается в доказательство — он только меняет границу, за которой мы соглашаемся ничего не проверять.
Что из этого следует
Дейкстра и Вирт сформулировали правило ещё в 1970-х:
Поскольку такого рода исчерпывающее экспериментальное тестирование и бессмысленно, и невозможно, мы можем сформулировать следующее важное, основное правило: экспериментальное тестирование программ может служить для доказательства наличия ошибок, но никогда не докажет их отсутствия.
Раз исчерпывающее тестирование невозможно — значит, цель не «проверить всё». Цель — описать спецификацию. Описание спецификации конечно. Его можно посчитать.
Стандарт ISTQB (International Software Testing Qualifications Board — международный совет по сертификации тестировщиков, базовый отраслевой стандарт по терминологии и методологии тестирования) определяет компонентное тестирование именно так: оценка компонента относительно его спецификации. Базис тестов — спецификация. Сколько утверждений в спецификации — столько и тестов.
Сколько тестов нужно. Формула
Возвращаемся к спецификации компонента. В ней два класса утверждений:
- Штатное поведение — одно утверждение: «при валидном запросе сервис делает то-то».
- Поведение при отказе каждой внешней связи — по утверждению на каждую различимую ветку обработки в адаптере.
Что такое «различимая ветка». Адаптер — это модуль, который инкапсулирует работу с внешней зависимостью (БД, брокером, внешним HTTP-сервисом). Адаптер возвращает результат — успех или ошибку. Если адаптер мапит «любую проблему с зависимостью» в одну ошибку — у него одна ветка. Если он различает таймаут (повторим запрос) и полную недоступность (сразу отдадим ошибку клиенту) — у него две ветки.
Решение «различать или нет» принимается при проектировании адаптера. И вместе с ним фиксируется число сценариев.
Формула
Почему именно по тесту на каждую различимую ветку. Спецификация — это контракт сервиса с внешним миром. Если адаптер обещает различать два типа ошибок (например, таймаут и недоступность), это два разных утверждения в контракте. Чтобы спецификация была описана полностью, каждое утверждение должно быть проверено отдельно. Один тест «на отказ вообще» не докажет, что адаптер на реальной инфраструктуре действительно различает эти случаи.
Простой пример
Сервис регистрации клиента. Интеграции:
- PostgreSQL — сохраняет клиента;
- Kafka — публикует событие
client_registered.
Адаптеры спроектированы просто: любая ошибка зависимости → одна ошибка наружу. Одна ветка в каждом.
1 (happy path)
+ 1 (БД недоступна → 503 клиенту)
+ 1 (брокер недоступен → 503 клиенту)
= 3 компонентных теста
Если в адаптере БД мы решили различать таймаут запроса и полную недоступность как разные ветки — добавляется ещё один сценарий:
Произвола нет. Каждое число обосновано конкретным решением при проектировании.
Как выглядят сценарии
Штатное поведение:
Scenario: Успешная регистрация совершеннолетнего клиента
Given сервис регистрации запущен
And БД доступна
And брокер сообщений доступен
When клиент отправляет POST /clients с валидными данными
Then ответ 201 Created с идентификатором клиента
And запись клиента сохранена в БД
And событие client_registered опубликовано в брокер
Отказ БД:
Scenario: Отказ БД при регистрации
Given сервис регистрации запущен
And БД недоступна
And брокер сообщений доступен
When клиент отправляет POST /clients с валидными данными
Then ответ 503 Service Unavailable
And событие client_registered не публиковалось
Отказ брокера:
Scenario: Отказ брокера при регистрации
Given сервис регистрации запущен
And БД доступна
And брокер сообщений недоступен
When клиент отправляет POST /clients с валидными данными
Then ответ 503 Service Unavailable
And запись клиента не сохранена в БД
Три сценария — три блока. Каждый описывает одно утверждение о контракте сервиса. Читается как русский текст. Прогоняется машиной — реальный сервис в Docker Compose, реальная БД, реальный брокер.
Что не нужно проверять компонентным тестом
Главная ошибка начинающих — раздувают компонентный уровень проверками, которые должны быть в других местах.
Сборка и запуск
Компонентный тест не проверяет, что сервис собирается и стартует. Это работа компилятора и сборщика — go build, mvn package, cargo build. Если сервис не собрался — компонентный тест даже не запустится. Не нужен отдельный «smoke test на запуск».
Валидация полей API
Это юнит-уровень. Проверка возраста (age >= 18), формата email, длины ФИО — всё это происходит внутри модулей при правильном проектировании. Каждый модуль контролирует диапазон допустимых значений на входе. Это и есть контроль диапазонов входных значений, о котором мы говорили в статье о правильности программ. Модуль обязан проверять, что вход допустим, прежде чем что-либо делать. Сам термин «защищённое программирование» появился позже, в индустрии 1970–80-х; идея же восходит к работам Хоара и Дейкстры о предусловиях.
Юнит-тест на чистых значениях, без поднятия инфраструктуры — это идеальное место для таких проверок.
Юнит-тест модуля валидации возраста:
возраст 17 → ошибка «младше 18 лет»
возраст 18 → успех
возраст 0 → ошибка «некорректное значение»
возраст 128 → не существует, тип int8 не допустит
Эти тесты не нужно дублировать на компонентном уровне. Если модуль валидации возраста работает — он работает и в составе сервиса.
Бизнес-логика и ветвления
То же самое. Если модуль регистрации клиента правильно вычисляет возраст по дате рождения — это доказывается юнитом. Если модуль решает, отправлять или нет письмо подтверждения — это юнит. Все ветвления внутри сервиса покрыты юнит-тестами на 100%.
Компонентный тест не повторяет это. Он не «проверяет работу системы целиком» в смысле «прогоняет все возможные комбинации». Он проверяет только контракт сервиса с внешним миром.
Когда правильность не достигнута проектированием
Если модули не проектируются правильно — если внутри них нет контроля диапазонов, если ошибки не возвращаются как данные, если зависимости не вынесены в адаптеры — тогда юнит-тесты не покрывают логику. И тогда возникает соблазн добавить компонентных тестов «на всякий случай», чтобы хоть как-то поймать проблемы.
Это и есть мифология тестирования: компенсация слабого проектирования количеством тестов.
Больше тестов — хуже. Это стоит денег.
Логика простая:
- Больше тестов — больше кода. Кто-то его пишет (или генерирует ИИ — это токены), кто-то проверяет (это человеко-часы).
- Больше компонентных тестов — больше ресурсов машины на исполнение. Каждый сценарий поднимает Docker Compose с реальными зависимостями.
- Больше прогонов — больше токенов и облачных мощностей. ИИ-агент, который анализирует результаты тестов, тоже работает не бесплатно.
На уровне одного разработчика с одним сервисом разница может быть незаметна. На уровне организации она становится критичной.
Простой пример масштаба. Компания: 50 команд, 50 сервисов, минимум один деплой в день на каждый. Каждый деплой прогоняет компонентные тесты в CI.
- Если на сервис приходится 5 сценариев по спецификации — это 250 прогонов в день в CI.
- Если на сервис приходится 50 сценариев «на всякий случай» — это 2500 прогонов в день.
Десятикратная разница в нагрузке на инфраструктуру, в счёте за CI, в человеко-часах на поддержку отвалившихся сценариев. И уверенности в коде — ровно столько же.
Конкретные замеры по времени и деньгам — отдельный эксперимент. Зафиксируем для следующей статьи серии: возьмём типовой сервис на фиксированных ресурсах, замерим время прогона набора тестов, экстраполируем на масштаб организации. Там будут цифры. Здесь — логика.
Правильный путь — вкладываться в проектирование. Тогда тестов нужно мало, и каждый из них — осмысленный.
Аналогия с электроникой
Зрелая инженерная дисциплина решает эту задачу больше ста лет.
Когда производитель сдаёт интегральную микросхему, к ней прилагается datasheet — техническая спецификация. Это не «список возможных багов». Это описание блока как чёрного ящика:
- что блок делает (логическая таблица);
- допустимые диапазоны входов (напряжение, ток, температура);
- поведение на границах (что происходит при выходе за диапазон);
- поведение при отказе внешних связей (отключилось питание, замкнуло ножку).
Транзисторы внутри микросхемы верифицированы на своём уровне — это юнит. Конвейерная проверка не повторяет проверку каждого транзистора. Она подаёт штатный сигнал, проверяет реакцию, подаёт отказ питания, проверяет реакцию — и сверяет с datasheet. Это компонентный тест.
В софте то же самое:
- Транзисторы — это модули внутри сервиса. Их верифицируют юнит-тесты.
- Datasheet — это Gherkin-сценарии. Они описывают поведение сервиса как чёрного ящика.
- Конвейерная проверка — это компонентные тесты в Docker Compose. Реальный сервис, реальные зависимости, проверка штатного поведения и отказов.
Datasheet — это документ, который читает инженер другой команды, чтобы интегрировать чип в свою плату. Он не лезет внутрь, он читает спецификацию на границе.
Точно та же роль у Gherkin-сценариев для вашего сервиса. Аналитик соседней команды, разработчик, который подключает ваш сервис к своему, ИИ-агент, который генерирует клиентский код — все они читают спецификацию и понимают, чего ожидать.
Что меняется для вайб-кодера
Если вы пришли в код из аналитики или тестирования через ИИ — вам, возможно, кажется, что предыдущий опыт «не нужен в новом мире». Это неправда. Он нужен, и нужен правильным способом.
Раньше было — стало
Раньше тест-кейсы писались в Excel или Jira. Их читал только тестировщик. Разработчик жил в своей реальности, тестировщик — в своей.
Теперь сценарии пишутся в Gherkin. Их читают разработчик, аналитик, вайб-кодер и машина. Один артефакт — общий язык.
Раньше тест проверял баг. «Если ввести в поле возраст −5, должна быть ошибка». Бесконечный список таких кейсов.
Теперь сценарий описывает контракт. «При валидных данных регистрация успешна. При недоступной БД — 503». Спецификация конечна и описуема.
Раньше тестировщик отвечал за качество. Разработчик «сделал», тестировщик «проверил». Качество появлялось в конце.
Теперь качество появляется в проектировании. Сценарии Gherkin пишутся до кода — это и есть постановка задачи. Разработчик и аналитик договорились о контракте через сценарий, потом разработчик пишет код под сценарий.
Что это означает для вашей работы
Если вы пришли из аналитики — ваша новая работа в том, чтобы писать Gherkin-сценарии. Это формализация требований, которой вы уже занимались, только в исполняемой форме. Машина прогонит ваш сценарий и скажет, соответствует ли реализация. Это сильнее любого Excel-кейса.
Если вы пришли из тестирования — ваша новая работа в том, чтобы проектировать спецификации компонентов вместе с разработчиком. Не «проверить готовое», а «договориться, что должно быть» и зафиксировать это в Gherkin. Это работа на входе, а не на выходе.
Если вы пишете код через ИИ — ваша работа в том, чтобы держать дисциплину. ИИ может за минуту сгенерировать сто тестов «на всякий случай». Это не помощь — это создание мусора, который придётся поддерживать. Дисциплина: один сценарий — одно утверждение из спецификации. Спецификация — конечна. Сценариев — ровно столько, сколько утверждений.
Резюме
При правильном проектировании сервиса:
-
Каждый модуль внутри контролирует диапазоны значений на входе и возвращает результат как данные (успех или ошибка). Логика покрыта юнит-тестами.
-
Компонентный тест проверяет только контракт сервиса с внешним миром: штатное поведение и отказ каждой внешней связи.
-
Формула:
1 + Σ (число различимых веток в адаптере i). -
Для типичного сервиса с двумя-тремя интеграциями получается 3–6 сценариев. Не двадцать. Не пятьдесят. Несколько.
-
Сценарии пишутся в Gherkin — на языке предметной области, понятно для аналитика, разработчика, вайб-кодера и машины.
Это не оптимизация количества тестов. Это следствие правильного проектирования и определения теста как исполняемой спецификации.
Что дальше
В следующих частях разберём, как именно проектировать модуль так, чтобы это всё работало:
- как контролировать диапазоны значений на входе и выходе каждого модуля;
- как возвращать ошибку как данные и компоновать модули через результат;
- как тестировать интеграционно и контрактно — и почему этих тестов тоже немного.
Сначала — логика и спецификация. Потом — проектирование. Потом — практика.
Источники
- Dijkstra E. W. A Discipline of Programming. Prentice-Hall, 1976.
- Wirth N. Systematic Programming: An Introduction. Prentice-Hall, 1973.
- Linger R. C., Mills H. D., Witt B. I. Structured Programming: Theory and Practice. Addison-Wesley, 1979.
- Meyer B. Applying "Design by Contract". IEEE Computer, 1992.
- Adzic G. Specification by Example. Manning, 2011.
- ISTQB Foundation Level Syllabus. Раздел Component Testing.
- Morev M. Правильность программы. codemonsters.team, 2025.
- Morev M. Модульность программы. codemonsters.team, 2025.
- Morev M. README — это продукт. codemonsters.team, 2026.