Где провести границу между юнит-тестами и интеграционными тестами
· Го Комура · Тестирование, Юнит-тесты, Интеграционные тесты, Проектирование тестов, Разработка Windows, C# / .NET
В разговорах о проектировании тестов раз за разом незаметно сложным оказывается один и тот же вопрос: сколько заталкивать в юнит-тесты, а с какого момента поднимать проверку до интеграционных тестов.
Опасны здесь две крайности.
- Хочется быстрого цикла — и всё превращается в юнит-тесты
- Хочется близости к реальности — и всё превращается в интеграционные тесты
Первая крайность обрастает mock-ами и легко упускает именно те точки, что ломаются в проде; вторая обычно превращается в медленный и хрупкий набор тестов. На практике оси, на которые стоит смотреть, гораздо понятнее.
- Вы хотите проверить свою логику или связь с внешним миром?
- Если подставить in-memory fake, сохранится ли смысл проверки?
- Реальное поведение БД / файлов / HTTP / DI / конфигурации / фреймворка / ОС — это и есть предмет проверки?
- Хотите ли вы быстро прогонять большое число входных вариантов?
Как только эти четыре пункта видны, границу между юнит-тестами и интеграционными тестами провести становится намного проще.
В этой статье мы разбираем границу между юнит-тестами и интеграционными тестами с практическим уклоном, опираясь на открытые материалы Microsoft Learn и Мартина Фаулера, доступные по состоянию на март 2026 года.123
1. Сначала вывод
Если сформулировать довольно грубо, но удобно для практики, получится так.
- Чистая логика — в юнит-тесты
- Соединения, связи, преобразования и различия среды — в интеграционные тесты
- Если проверить можно на любом уровне — начинайте с юнит-теста
- Интеграционные тесты лучше не делать широкими и тяжёлыми, а сужать до границы
Одной фразой: юнит-тесты — это «тесты решений», интеграционные тесты — это «тесты связей».
Вещи, чей смысл самодостаточен без внешних ресурсов, — расчёт суммы, переходы состояний, валидация ввода, условия одобрения, классификация исключений, — быстрее, устойчивее и позволяют гораздо плотнее прогонять варианты входных данных, если их сдвинуть в сторону юнит-тестов. С другой стороны, вещи, которые «подводят именно в момент соединения», — выполнение SQL, сериализация JSON / CSV, маршрутизация, привязка модели, регистрация в DI, файловые блокировки, права доступа, регистрация COM, разрядность 32/64 бит, STA / MTA, — безопаснее размещать на стороне интеграционных тестов.
В материале Microsoft Learn Integration tests in ASP.NET Core тоже рекомендуется сужать интеграционные тесты до важных инфраструктурных сценариев и выбирать юнит-тесты там, где их достаточно.
2. Что в этой статье называется юнит-тестом и интеграционным тестом
Здесь термины используются так.
| Уровень | Что проверяет | Типичная конфигурация |
|---|---|---|
| Юнит-тест | Корректность одной изолированной ответственности | Использует fake / mock / stub, отсекает внешние ресурсы |
| Интеграционный тест | Связи между несколькими компонентами и поведение, включающее инфраструктуру и фреймворки | Реальная БД, реальные файлы, реальный сериализатор, реальный хост, реальный конвейер и т. п. |
| E2E / функциональный тест | Пользовательский поток через всё приложение | Развёрнутое приложение, несколько сервисов, реальный браузер или реальный процесс |
В материалах .NET по юнит-тестированию хороший юнит-тест описывается как fast / isolated / repeatable — не зависящий от внешних факторов вроде файловой системы или БД. Подробнее об этом понятно рассказано в Unit testing best practices for .NET.
Кроме того, интеграционный тест — это не обязательно только «тяжёлый тест, который непременно использует другой процесс или другой сервер». Даже в пределах одного процесса, если вы соединяете несколько реальных компонентов и проверяете подлинное поведение фреймворка или инфраструктуры, это уже тяготеет к интеграционному тесту.
Например, при юнит-тестировании controller action в ASP.NET Core официальная рекомендация — сузить предмет проверки до решений внутри самого action, а взаимодействие с фреймворком вроде routing, model binding, filters обрабатывать интеграционными тестами. Подробный разбор есть в Unit test controller logic in ASP.NET Core.
3. Таблица решений на одном листе
Сначала — таблица, наиболее удобная на практике.
| Что хотите проверить | Основной тест | Примечание |
|---|---|---|
| Расчёт суммы, скидки, переходы состояний, валидация ввода | Юнит-тест | Хочется плотно прогонять варианты ввода |
| Классификация исключений, выбор текста ошибки, решение о повторе | Юнит-тест | Смысл самодостаточен без реального I/O |
| SQL / ORM-преобразование в Repository, транзакции | Интеграционный тест | Предмет — поведение реальной БД или реального провайдера |
| Сериализация / десериализация JSON / XML / CSV | Интеграционный тест | Расхождения в wire-формате трудно найти через fake |
| Маршрутизация, привязка модели, фильтры, middleware | Интеграционный тест | Проверка соединения с фреймворком |
| Переходы состояний ViewModel или Presenter в WPF / WinForms | Юнит-тест | Осмысленно даже без поднятия UI |
| Реальный Binding, Dispatcher, жизненный цикл control, message loop | Интеграционный тест или UI-тест | Предмет — поведение фреймворка и потоков |
| Пути к файлам, права доступа, блокировки, общие папки, символы конца строки, кодировки | Интеграционный тест | Нужно реальное поведение ОС и файловой системы |
| Регистрация COM, разрядность 32/64 бит, STA / MTA, источник загрузки DLL | Интеграционный тест | Предмет — различия среды и границы процессов |
| Запуск приложения целиком, сквозная проверка основных сценариев использования | E2E / smoke | Достаточно небольшого числа тестов |
Секрет чтения этой таблицы — спрашивать, какой тест ближе всего к «причине сбоя в проде». Решать, ориентируясь на неопределённость, которую хочется снизить, а не на расположение кода в проекте, — надёжнее.
4. Что должно быть в юнит-тестах
Юнит-тестам подходит ответственность, чей смысл сохраняется даже после удаления внешнего мира.
Например, это:
- бизнес-правила;
- ветвления;
- переходы состояний;
- валидация ввода;
- классификация ошибок;
- решение о политике повторов;
- изменения состояния ViewModel / Presenter;
- сама логика преобразования.
В особенности чем больше у чего-то комбинаций, тем выше ценность сдвига в сторону юнит-тестов.
Например,
- купон есть / нет;
- товар в наличии / нет;
- первый заказ / повторный заказ;
- администратор / обычный пользователь;
- корректное значение / граничное значение / некорректное значение —
чем больше таких условий ветвления, тем тяжелее прогонять их все через интеграционные тесты. Здесь рациональнее нарезать всё мелко юнит-тестами.
Кроме того, в юнит-тестах важно держать внешние факторы под контролем.
- Внедрять текущее время
- Делать GUID и случайные числа заменяемыми
- Не ждать через sleep
- Не трогать реальную БД или реальные файлы
- Не выходить в реальную сеть
Если это соблюдается, тесты становятся заметно стабильнее.
4.1. Когда в юнит-тесте становится слишком много mock-ов
Если при попытке написать юнит-тест оказывается, что
- нужно 7 mock-ов;
- setup получается длинным;
- arrange длиннее самого тела теста;
- непонятно, что вообще хотелось проверить, —
обычно верно одно из двух:
- у этого класса слишком много ответственностей;
- в юнит-тест затолкали связи, которые на самом деле нужно проверять интеграционным тестом.
Mock — это инструмент для отсечения внешнего мира, а не инструмент, доказывающий, что соединение с реальным компонентом работает правильно. Спутав это, легко получить ситуацию «всё зелёное, а прод падает».
5. Четыре границы, которые стоит поднять до интеграционных тестов
Места, которые стоит поднимать до интеграционных тестов, в целом делятся на четыре: форматы, связи, среда и время.
5.1. Граница форматов
Сюда относятся такие вещи:
- JSON / XML / CSV
- схема БД и mapping
- nullable / точность / часовой пояс
- сериализация enum и дат
- кодировки символов и BOM
- символы конца строки
Мартин Фаулер тоже относит границы, включающие сериализацию / десериализацию, к кандидатам на интеграционный тест. Подробнее об этом можно прочитать в The Practical Test Pyramid.
Например, такие дефекты, как
- у DTO после сериализации в JSON оказались другие имена полей,
- сломались кавычки или переносы строк в CSV,
decimalокруглился,- обращение с
DateTimeOffsetв БД поехало, nullи пустая строка обработались не так, как ожидалось, —
легко проскакивают мимо одних только юнит-тестов.
5.2. Граница связей
К границе связей относятся, например, такие части:
- регистрация в DI
- привязка конфигурации
- маршрутизация
- привязка модели
- фильтры
- middleware
- запуск хоста
- связывание событий
- Binding и привязка команд в WPF
Здесь предмет — не «правильна ли моя функция», а правильно ли соединены между собой несколько реальных компонентов.
В ASP.NET Core официальная рекомендация — сужать юнит-тесты controller action до решений самого action, а routing, model binding и filters смотреть на стороне интеграционных тестов.
Вне веба логика та же: в десктопном приложении переходы состояний ViewModel относятся к юнит-тестам, а поведение с реальным XAML Binding или Dispatcher — уже тяготеет к интеграционным тестам.
5.3. Граница среды
В разработке под Windows это имеет очень большое значение.
- права доступа к файлам
- общие папки
- блокировки файлов
- переименование из временного файла
- права администратора
- права на запуск службы
- регистрация COM
- разрядность 32/64 бит
- STA / MTA
- источник загрузки DLL
Здесь главные действующие лица — сами условия ОС и среды выполнения. В in-memory fake смысл этого сильно теряется, поэтому безопаснее покрывать такие случаи интеграционными тестами.
Особенно в конфигурациях, включающих существующее Windows-ПО или COM / ActiveX, вполне обычное дело — споткнуться раньше, чем до логики вообще дойдёт очередь, именно на регистрации, разрядности, модели потоков и правах доступа. Такие сбои — территория интеграционных тестов, учитывающих среду, а не юнит-тестов.
5.4. Граница времени
Ещё одна вещь, которую легко упустить, — время и параллелизм.
- таймауты
- отмена операций
- реальное поведение повторов
- обработка, управляемая таймером
- остановка фоновой обработки
- гонки (race condition)
- порядок завершения при shutdown
Здесь важно разделять решение и реальное поведение.
Например,
- сколько раз повторять попытку;
- какие исключения считать поводом для повтора —
вполне достаточно проверить юнит-тестами. А вот
- действительно ли срабатывает таймаут;
- распространяется ли отмена;
- не ломается ли всё при столкновении таймера с асинхронной обработкой;
- аккуратно ли закрываются хендлы и задачи при завершении —
тяготеет к интеграционным тестам.
6. Частые ошибки в решениях
6.1. Замокать Repository и успокоиться на этом
Даже если всё вокруг Repository проходит через mock, вы всё равно не знаете,
- корректен ли SQL;
- срабатывают ли транзакции;
- соответствует ли это схеме;
- не расходится ли mapping;
- не ломаются ли кодировки и точность.
Repository чаще является не столько объектом тестирования логики, сколько точкой соединения на границе. В этом случае реальности лучше соответствует смещение веса в сторону интеграционных, а не юнит-тестов.
6.2. Попытка увидеть фреймворк в юнит-тесте контроллера / endpoint
В юнит-тесте controller action хочется увидеть примерно вот что:
- условные ветвления;
- выбор возвращаемого значения;
- какой из зависимых сервисов вызывается.
А вот
- срабатывает ли маршрут;
- проходит ли привязка модели;
- срабатывает ли фильтр;
- как выглядит результат после прохождения middleware —
это уже сторона интеграционных тестов. Если это смешать, становится трудно понять, что именно сломалось.
6.3. Перебор всех входных вариантов в интеграционных тестах
Интеграционные тесты, будучи ближе к реальности, неизбежно медленнее. Поэтому выгоднее разделить: перебор ветвлений — в юнит-тесты, представительные случаи на границе — в интеграционные тесты.
В описании интеграционных тестов на Microsoft Learn тоже рекомендуется для БД и файловой системы не гонять все паттерны через интеграционные тесты, а сужать их до представительных сценариев вроде чтения / записи / обновления / удаления.
6.4. Обращение из CI напрямую к продакшн-версии внешних сервисов
Этого лучше избегать.
В интеграционных тестах важна «реалистичность», но это не значит, что нужно каждый раз обращаться к продакшн SaaS или продакшн API. Фаулер тоже рекомендует поднимать внешние сервисы локально, размещать fake или использовать выделенный тестовый экземпляр.
На практике удобно сочетание:
- локальной БД;
- временных директорий;
- тестового хоста;
- выделенной тестовой среды;
- fake-сервиса с зафиксированным контрактом.
7. Рекомендуемая структура на практике
Абсолютно правильного соотношения не существует. Но достаточно универсально применима такая структура из трёх слоёв.
| Слой | Основной инструмент | Что туда помещать |
|---|---|---|
| Слой ядра | Плотные юнит-тесты | Бизнес-правила, переходы состояний, валидация ввода, классификация ошибок |
| Слой границ | Узкие интеграционные тесты | БД, файлы, HTTP, сериализатор, DI, конфигурация, COM, права доступа |
| Слой целого | Небольшое число smoke / E2E тестов | Проверка запуска, основные потоки, предотвращение повторения серьёзных инцидентов |
Интуитивно: юнит-тесты набирают плотность количеством, интеграционные тесты — плотностью покрытия границ.
Рекомендуемый порядок действий такой.
- Сначала перечислить границы приложения
- Привести логику к виду, отделимому от внешнего мира
- На каждой границе разместить «минимум один happy path» и «представительный сценарий сбоя»
- Свести число сквозных прогонов к минимуму
- При появлении бага добавить тест на том слое, где этот баг воспроизводится с минимальными затратами
Последний, пятый пункт — самый важный.
- Если это ошибка правила — добавьте юнит-тест
- Если это ошибка SQL / привязки / конфигурации / прав доступа / регистрации — добавьте интеграционный тест
- Если это сбой, включающий запуск или распространение приложения, — добавьте smoke- или E2E-тест
При таком способе роста ответственность тестов не расплывается.
8. Пять вопросов на случай сомнений
Напоследок — пять вопросов, собранных для проверки в случае сомнений.
- Если подставить in-memory fake, сохранится ли смысл, который вы хотели проверить?
- Если сохраняется — это тяготеет к юнит-тесту.
- Если что-то сломается, заподозрите ли вы скорее соединение или настройку, а не логику?
- Если да — это тяготеет к интеграционному тесту.
- Не является ли предметом проверки БД / файлы / сериализатор / DI / маршрут / привязка модели / ОС / права доступа / разрядность / потоки?
- Если является — это тяготеет к интеграционному тесту.
- Хотите ли вы быстро прогонять большое число входных вариантов?
- Если да — это тяготеет к юнит-тесту.
- Когда этот тест падает, сразу ли понятно, что нужно исправить?
- Если непонятно — слои тестов перемешаны.
Разобравшись по этим пяти вопросам, легче избежать небрежных решений вроде «интеграционный тест, потому что как-то ближе к реальности» или «юнит-тест, потому что как-то быстрее».
9. Итог
Границу между юнит-тестами и интеграционными тестами практичнее всего определять не по расположению кода, а по тому, какую неопределённость вы хотите снизить.
Суть сводится к этим пяти пунктам.
- Юнит-тесты — это тесты решений
- Интеграционные тесты — это тесты связей
- Перебор ветвлений — в юнит-тесты
- Форматы, связи, среда и время — в интеграционные тесты
- Сквозную проверку целого покрывайте небольшим числом smoke / E2E тестов
Больше всего стоит избегать трёх вещей:
- считать, что mock доказал корректность соединения с реальным компонентом;
- пытаться прогнать все ветвления через интеграционные тесты;
- смешивать ответственность юнит-тестов и интеграционных тестов.
Если сомневаетесь, сначала спросите себя: этот дефект ломает «решение» или ломает «связь»? Один этот вопрос разрешает значительную часть случаев.
10. Похожие статьи
- Минимальный чек-лист безопасности при разработке Windows-приложений
- Распространение Windows-приложения одним файлом — единый бинарный файл и пределы зависимости от ОС
- Когда на Windows действительно требуются права администратора — UAC, защищённые области и как это определить на этапе проектирования
- Что такое Reg-Free COM — механизм использования COM без регистрации
11. Справочные материалы
-
Microsoft Learn, Integration tests in ASP.NET Core ↩
-
Microsoft Learn, Unit testing best practices for .NET ↩
-
Martin Fowler, The Practical Test Pyramid ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Минимальные требования к самописному логгеру и чек-лист интеграционных тестов
Чтобы диагностические логи собственного приложения заслуживали доверия, разбираем формат UTF-8 JSON Lines, обязательные поля, flush, рота...
Как ускорить проверку приложений с помощью Windows Sandbox
Разбираем, как использовать Windows Sandbox для эффективной локализации проблем с правами администратора, воспроизведения сценариев в чис...
Таблица решений: завершать работу приложения или продолжать при неожиданном исключении
Разбираем, когда после неожиданного исключения приложение стоит завершать, а когда можно продолжать работу — с точки зрения повреждения с...
Как на практике выделить в Windows-приложении «только те операции, для которых нужны права администратора»
Разбираем на практике дизайн, при котором UI Windows-приложения остаётся asInvoker, а операции, требующие прав администратора, выносятся ...
Хранение секретов в Windows-приложениях - избегаем открытых настроек с помощью DPAPI
Чтобы не хранить учётные данные и API-токены в конфигурационных файлах Windows-приложений в открытом виде, разбираем принципы DPAPI / Pro...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Технические консультации и ревью дизайна
Вопрос, где именно провести границу между юнит-тестами и интеграционными тестами, удобно разбирать в формате ревью проектирования до реализации или консультации по тестовой стратегии.
Разработка приложений для Windows
В Windows-приложениях границы вроде файлов, прав доступа, COM, разрядности 32/64 бит напрямую отражаются на слоях тестирования, поэтому это хорошо сочетается с выработкой подхода к реализации.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- В чём разница между юнит-тестом и интеграционным тестом?
- Юнит-тест проверяет корректность одной изолированной ответственности, используя fake, mock или stub, чтобы отсечь внешние ресурсы вроде баз данных и файлов. Интеграционный тест проверяет связи между несколькими компонентами и поведение, включающее реальную инфраструктуру или фреймворки, — реальную БД, реальные файлы, реальный конвейер хоста. Одной фразой: юнит-тесты — это тесты решений, интеграционные тесты — тесты связей.
- Как решить, куда отнести проверку — к юнит-тесту или к интеграционному тесту?
- Спросите себя, сохраняется ли смысл того, что вы хотите проверить, если подставить in-memory fake. Чистая логика вроде расчёта цены, переходов состояний, валидации ввода и классификации ошибок самодостаточна без внешних ресурсов и относится к юнит-тестам. Вещи, которые подводят именно в момент соединения — выполнение SQL, сериализация JSON или CSV, маршрутизация, регистрация в DI, файловые блокировки, права доступа, регистрация COM, — безопаснее проверять интеграционными тестами. Если проверить можно на любом уровне, начинайте с юнит-теста.
- Что означает, если юнит-тесту требуется много mock-объектов?
- Если юнит-тесту нужно семь mock-ов, длинная настройка, или раздел arrange длиннее самого теста, это обычно один из двух симптомов. Либо у класса слишком много ответственностей, либо вы затаскиваете в юнит-тест связи, которые на самом деле должен проверять интеграционный тест. Mock — это инструмент для отсечения внешнего мира, а не для доказательства того, что соединение с реальным компонентом работает правильно; спутав это, легко получить набор тестов, где всё зелёное, а прод падает.
- Нужно ли интеграционным тестам покрывать все варианты входных данных?
- Нет. Интеграционные тесты ближе к реальности и потому медленнее, поэтому перебор всех веток через них даёт медленный и хрупкий набор тестов. Практичное разделение — прогонять большое число входных вариантов быстро через юнит-тесты, а интеграционные тесты сужать до представительных сценариев на каждой границе, например один happy path и один характерный сценарий сбоя. Рекомендации Microsoft тоже советуют для базы данных и файловой системы не гонять все паттерны через интеграционные тесты, а сужать их до представительных сценариев чтения, записи, обновления и удаления.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки