Где провести границу между юнит-тестами и интеграционными тестами
· Обновлено: · Го Комура · Тестирование, Юнит-тесты, Интеграционные тесты, Проектирование тестов, Разработка Windows, C# / .NET
История изменений (5 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Переведены имена методов в примерах xUnit. Утверждения статьи не менялись.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- В начало статьи добавлен раздел «Карта знаний этой статьи». Понятия из текста и связи между ними собраны в краткое изложение, схему и ссылку на страницу сведений. Утверждения статьи не менялись.
- Текст обновлён по результатам внешнего ревью (1283 замечания). Содержание отдельных правок — в записях ниже.
- В начале явно сказано, что статья исходит из C# и .NET, а примеры кода — на xUnit. Добавлены минимальные примеры юнит-теста и интеграционного теста, таблица видов тестовых двойников (stub, mock, fake) и связь с формулировкой «нужно 7 mock-ов». Отдельно зафиксировано, как эта статья трактует E2E.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619801)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). Где провести границу между юнит-тестами и интеграционными тестами. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/03/25/004-unit-test-vs-integration-test-boundary-guide/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21619801
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21619802
В разговорах о проектировании тестов каждый раз тихо сложно одно и то же: сколько запихивать в юнит-тесты и с какого момента поднимать проверку в интеграционные.
Опасно здесь два края:
- хочется быстрого цикла — и всё становится юнит-тестами
- хочется близости к реальности — и всё становится интеграционными тестами
Первое обрастает mock-ами и легко пропускает как раз те места, что ломаются в проде; второе обычно даёт медленный и хрупкий набор тестов. На практике оси, на которые стоит смотреть, заметно яснее.
- Вы хотите проверить свою логику или соединение с внешним миром?
- Если подставить in-memory fake, смысл проверки не пропадает?
- Поведение БД / файлов / HTTP / DI / конфигурации / фреймворка / ОС — это и есть предмет проверки?
- Нужно быстро прогнать много входных вариантов?
Как только эти четыре пункта видны, границу между юнит-тестами и интеграционными тестами провести гораздо проще.
Статья исходит из проектирования автотестов на C# / .NET. Примеры кода — на xUnit, но сами критерии от фреймворка не зависят. Тот же ход мысли читается и в JUnit, и в pytest.
Содержание опирается на то, что на март 2026 года доступно в Microsoft Learn: «Integration tests in ASP.NET Core»1, «Unit testing best practices for .NET»2 и «The Practical Test Pyramid» Мартина Фаулера3. Дальше, чтобы ссылки не сводились к одним номерам, в тексте называем источники по имени. URL первоисточников собраны в справочных материалах в конце статьи.
1. Сначала вывод
Грубо, но так, чтобы этим можно было пользоваться:
- Чистая логика — в юнит-тесты
- Соединения, связка, преобразования и различия среды — в интеграционные тесты
- Если проверить можно и там, и там — начинайте с юнит-теста
- Интеграционные тесты лучше не раздувать вширь и в тяжесть, а сужать до границы
Одной фразой: юнит-тесты — это «тесты решений», интеграционные тесты — «тесты соединений».
Расчёт суммы, переходы состояний, проверка ввода, условия одобрения, классификация исключений — то, чей смысл завершается без внешних ресурсов, — быстрее, устойчивее и позволяет плотнее гонять входные варианты, если сдвинуть их в юнит-тесты. С другой стороны, выполнение SQL, сериализация JSON / CSV, маршрутизация, привязка модели, регистрация в DI, файловые блокировки, права доступа, регистрация COM, 32-bit / 64-bit, STA / MTA — то, что «подводит в момент соединения», — безопаснее держать на стороне интеграционных тестов.
В материале Microsoft Learn Integration tests in ASP.NET Core тоже советуют сужать интеграционные тесты до важных инфраструктурных сценариев и выбирать юнит-тесты там, где их достаточно.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 18, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Что в этой статье называется юнит-тестом и интеграционным тестом
Термины здесь такие.
| Уровень | Что проверяет | Типичная конфигурация |
|---|---|---|
| Юнит-тест | Корректность одной изолированной обязанности | fake / mock / stub, внешние ресурсы отсечены |
| Интеграционный тест | Соединения нескольких компонентов и поведение, в котором участвуют инфраструктура и фреймворки | Реальная БД, реальные файлы, реальный serializer, реальный host, реальный pipeline и т. п. |
| 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.
2.1. Чем отличаются fake / mock / stub
В таблице выше стоят fake / mock / stub рядом, но это не одно и то же. Как виды тестовых двойников (заменителей для теста) в этой статье они значат следующее. Разделение совпадает с тем, как его проводит Мартин Фаулер в «Mocks Aren’t Stubs».4
| Название | Что делает | Типичное место |
|---|---|---|
| stub | Заменитель, который только возвращает заранее заданное значение. Как его вызвали — не проверяют | Когда нужно зафиксировать вход, например «остаток всегда 3» |
| mock | Заменитель, у которого проверяют сам вызов. Assert на число вызовов и аргументы | Когда нужно убедиться, что сохранение вызвали ровно один раз |
| fake | То же поведение, что у настоящего объекта, но лёгкая реализация | In-memory репозиторий, файловое хранилище во временном каталоге |
Грубо: stub подменяет вход, mock проверяет вызов, fake — упрощённая реализация. Это различие начинает работать в главе 4. Признак «нужно 7 mock-ов» точнее значит семь объектов, у которых проверяют вызовы, и это сигнал, что тест смотрит не на одно решение, а на то, как соединены несколько частей.
2.2. Как эта статья трактует E2E
Тема статьи — именно граница между юнит-тестами и интеграционными тестами. E2E / функциональные тесты стоят в таблице выше для контраста; в тексте их глубоко не разбираем.
По месту в пирамиде тестов: чем ниже слой, тем больше тестов и тем они быстрее; чем выше — тем меньше и тем они медленнее. Исходная постановка — у Мартина Фаулера в «The Practical Test Pyramid».3 То есть юнит-тесты как основание, интеграционные — по границам, E2E — только на основные потоки. Конкретная раскладка — в трёхслойной схеме главы 7.
3. Таблица решений на одном листе
Сначала — таблица, которой на практике удобнее всего пользоваться.
| Что хотите проверить | Основной тест | Заметка |
|---|---|---|
| Расчёт суммы, скидки, переходы состояний, проверка ввода | Юнит-тест | Хочется плотно гонять входные варианты |
| Классификация исключений, выбор текста ошибки, решение — повторять ли попытку | Юнит-тест | Смысл завершается без реального I/O |
| Преобразование SQL / ORM в Repository, transaction | Интеграционный тест | Предмет — поведение реальной БД или реального provider |
| serialize / deserialize JSON / XML / CSV | Интеграционный тест | Расхождение wire format через fake почти не видно |
| Маршрутизация, привязка модели, фильтры, middleware | Интеграционный тест | Проверка соединения с фреймворком |
| Переходы состояний ViewModel или Presenter в WPF / WinForms | Юнит-тест | Смысл есть и без поднятого UI |
| Настоящий Binding, Dispatcher, control lifecycle, message loop | Интеграционный тест или UI-тест | Предмет — поведение фреймворка и потоков |
| Пути к файлам, права доступа, блокировки, общие папки, символы конца строки, кодировки | Интеграционный тест | Нужно реальное поведение ОС и файловой системы |
| Регистрация COM, 32-bit / 64-bit, STA / MTA, откуда грузится DLL | Интеграционный тест | Предмет — различия среды и границы процессов |
| Запуск приложения целиком, сквозная проверка основных сценариев | E2E / smoke | Число тестов может быть небольшим |
Секрет чтения таблицы — спрашивать, какой тест ближе всего к «причине, по которой это ломается в проде». Решать по той неопределённости, которую хотите снизить, а не по тому, где лежит код, — так меньше плывёт.
4. Что должно жить в юнит-тестах
Юнит-тестам подходит обязанность, у которой смысл остаётся, даже если внешний мир убрать.
Например:
- бизнес-правила
- ветвления
- переходы состояний
- проверка ввода
- классификация ошибок
- решение о политике повторов
- изменение состояния ViewModel / Presenter
- сама логика преобразования
Особенно чем больше комбинаций, тем выше цена сдвига в юнит-тесты.
Например,
- купон есть / нет
- товар в наличии / нет
- первый заказ / повторный
- администратор / обычный пользователь
- нормальное значение / граничное / некорректное
чем больше таких условий, тем тяжелее гонять их все интеграционными тестами. Здесь разумнее нарезать мелко юнит-тестами.
Кроме того, в юнит-тестах важно держать внешние факторы под контролем.
- Текущее время внедрять
- GUID и случайные числа делать подменяемыми
- Не ждать через sleep
- Не трогать реальную БД и реальные файлы
- Не выходить в реальную сеть
Если это соблюдается, тесты заметно стабильнее.
4.1. Когда в юнит-тесте слишком много mock-ов
Если при попытке написать юнит-тест оказывается, что
- нужно 7 mock-ов
- setup длинный
- arrange длиннее самого тела теста
- непонятно, что хотели проверить
обычно верно одно из двух:
- у класса слишком много обязанностей
- в юнит-тест затолкнули связку, которую на самом деле должен проверять интеграционный тест
Mock — инструмент, чтобы отсечь внешний мир, а не инструмент, который доказывает, что соединение с настоящим компонентом работает. Если это перепутать, легко получить «всё green, а в проде падает».
4.2. Минимальный пример юнит-теста
На одних словах это абстрактно, поэтому поставим по одному примеру кода на каждую сторону границы. Сначала юнит-тест. Переход состояния ViewModel завершается по смыслу и без UI, поэтому это территория юнит-теста.
// .NET 8 / xUnit
// Объект: ViewModel, который вообще не трогает внешние ресурсы
public sealed class OrderViewModel
{
public decimal Subtotal { get; set; }
public bool IsMember { get; set; }
public bool CanCheckout => Subtotal > 0m;
public decimal Total => IsMember ? Subtotal * 0.9m : Subtotal;
}
public class OrderViewModelTests
{
[Fact]
public void УЧленаПрименяетсяСкидка10Процентов()
{
var viewModel = new OrderViewModel { Subtotal = 1000m, IsMember = true };
Assert.Equal(900m, viewModel.Total);
}
[Theory]
[InlineData(0, false)]
[InlineData(1, true)]
public void ПриНулевомПодытогеНельзяОформитьЗаказ(int subtotal, bool expected)
{
var viewModel = new OrderViewModel { Subtotal = subtotal };
Assert.Equal(expected, viewModel.CanCheckout);
}
}
Здесь нет ни БД, ни файлов, ни HTTP. Поэтому тесты быстрые, их можно гонять параллельно, а [Theory] позволяет сколько угодно наращивать входные варианты.
«Полный перебор ветвлений сдвигайте сюда» на практике выглядит именно так.
5. Четыре границы, которые стоит поднимать в интеграционные тесты
Места, которые стоит поднимать в интеграционные тесты, в целом складываются в четыре: формат, связка, среда, время.
5.1. Граница форматов
В формат здесь входят такие вещи:
- JSON / XML / CSV
- schema и mapping БД
- nullable / precision / timezone
- сериализация enum и дат
- кодировки и BOM
- символы конца строки
Мартин Фаулер тоже относит границы, где есть serialize / deserialize, к кандидатам на интеграционный тест. Подробнее — в The Practical Test Pyramid.
Например,
- у DTO после сериализации в JSON оказались другие имена полей
- в CSV сломались кавычки или переносы строк
decimalокруглился- в БД поехало обращение с
DateTimeOffset nullи пустая строка обработались не так, как ждали
такие дефекты юнит-тестами легко пропустить.
5.2. Граница связки
На границу связки попадают, например, такие части:
- регистрация в DI
- bind конфигурации
- маршрутизация
- привязка модели
- фильтры
- middleware
- запуск host
- связка событий
- Binding и подключение command в WPF
Здесь предмет не «верна ли моя функция», а правильно ли соединены несколько реальных частей.
В ASP.NET Core официально: юнит-тест controller action сужают до решений самого action, а routing, model binding и filters смотрят интеграционными тестами.
Вне веба ход тот же: в десктопном приложении переходы состояний ViewModel — юнит-тесты, поведение с настоящим XAML Binding или Dispatcher — уже ближе к интеграционным.
5.3. Граница среды
В разработке под Windows этот пункт особенно важен.
- права на файлы
- общие папки
- блокировки файлов
- rename из временного файла
- права администратора
- права на запуск службы
- регистрация COM
- 32-bit / 64-bit
- STA / MTA
- откуда грузится DLL
Здесь главные действующие лица — сами условия ОС и среды выполнения. На in-memory fake смысл сильно падает, поэтому безопаснее закрывать такие случаи интеграционными тестами.
Особенно в конфигурациях с существующим Windows-ПО или COM / ActiveX обычное дело — споткнуться раньше логики, на регистрации, bitness, модели потоков и правах доступа. Такие сбои подбирает не юнит-тест, а интеграционный тест, который включает среду.
5.4. Граница времени
Ещё одно, что легко упустить, — время и параллелизм.
- timeout
- cancellation
- реальное поведение retry
- обработка по timer
- остановка фоновой работы
- race condition
- порядок завершения при shutdown
Здесь важно отделять решение от реального поведения.
Например,
- сколько раз повторять попытку
- какие исключения считать поводом для повтора
достаточно проверить юнит-тестами. А вот
- действительно ли срабатывает timeout
- распространяется ли cancellation
- не ломается ли всё, когда timer сталкивается с асинхронной обработкой
- аккуратно ли закрываются handle и задачи при завершении
ближе к интеграционным тестам.
5.5. Минимальный пример интеграционного теста
Тот же сюжет, что в 4.2, но с другой стороны границы. Здесь проверяем: «сохранённая сумма не ломается, даже если сходить в БД и вернуться». Проход идёт через настоящий файл SQLite.
// .NET 8 / xUnit / Microsoft.Data.Sqlite
using System.Globalization;
using Microsoft.Data.Sqlite;
public sealed class OrderRepository(SqliteConnection connection)
{
public void Save(int id, decimal total)
{
using var command = connection.CreateCommand();
command.CommandText = "INSERT INTO orders (id, total) VALUES ($id, $total);";
command.Parameters.AddWithValue("$id", id);
command.Parameters.AddWithValue("$total", total.ToString(CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
public decimal FindTotal(int id)
{
using var command = connection.CreateCommand();
command.CommandText = "SELECT total FROM orders WHERE id = $id;";
command.Parameters.AddWithValue("$id", id);
var stored = (string)command.ExecuteScalar()!;
return decimal.Parse(stored, CultureInfo.InvariantCulture);
}
}
public sealed class OrderRepositoryTests : IDisposable
{
private readonly string _databasePath =
Path.Combine(Path.GetTempPath(), $"orders-{Guid.NewGuid():N}.db");
private readonly SqliteConnection _connection;
public OrderRepositoryTests()
{
// Без Pooling=False файл БД иногда не удаётся удалить при зачистке
_connection = new SqliteConnection($"Data Source={_databasePath};Pooling=False");
_connection.Open();
using var create = _connection.CreateCommand();
create.CommandText = "CREATE TABLE orders (id INTEGER PRIMARY KEY, total TEXT NOT NULL);";
create.ExecuteNonQuery();
}
[Fact]
public void СохраненнаяСуммаЧитаетсяОбратноБезОкругления()
{
var repository = new OrderRepository(_connection);
repository.Save(id: 1, total: 1234.56m);
Assert.Equal(1234.56m, repository.FindTotal(1));
}
public void Dispose()
{
_connection.Dispose();
File.Delete(_databasePath);
}
}
Этот тест проверяет не ветвления OrderRepository. Он проверяет соединение: проходит ли SQL, сходится ли тип столбца с decimal, сохраняется ли значение после круга туда-обратно. У SQLite нет типа decimal, поэтому «в каком типе сохранять, чтобы значение не ломалось» — уже не вопрос реализации, а вопрос проектирования соединения. На in-memory fake этого не видно.
В примере каждый тестовый класс поднимает один временный файл БД и удаляет его в Dispose. У интеграционного теста есть состояние, поэтому каждый раз явно решать, где создать и где выбросить, важнее, чем в юнит-тесте.
6. Частые ошибки в решении
6.1. Замокать Repository и на этом успокоиться
Даже если всё вокруг Repository проходит через mock, вы всё равно не знаете,
- корректен ли SQL
- срабатывает ли transaction
- совпадает ли это со schema
- не расходится ли mapping
- не ломаются ли кодировка и precision
Repository чаще не объект тестирования логики, а точка соединения на границе. Тогда реальности лучше соответствует сместить вес с юнит-тестов на интеграционные.
6.2. В юнит-тесте controller / endpoint пытаться увидеть ещё и фреймворк
В юнит-тесте controller action смотреть стоит примерно на это:
- условные ветвления
- выбор возвращаемого значения
- какой из зависимых сервисов вызывается
А вот
- попадает ли route
- проходит ли model binding
- срабатывает ли filter
- как выглядит результат после middleware
уже сторона интеграционных тестов. Если это смешать, становится трудно понять, что именно сломалось.
6.3. Полный перебор входных вариантов интеграционными тестами
Интеграционные тесты ближе к реальности и потому неизбежно медленнее. Поэтому выгоднее разделить: полный перебор ветвлений — юнит-тесты, представительные случаи на границе — интеграционные.
В разборе интеграционных тестов на Microsoft Learn тоже советуют для БД и файловой системы не гонять все шаблоны интеграционными тестами, а сужать до представительных сценариев вроде read / write / update / delete.
6.4. Из CI ходить напрямую в прод внешних сервисов
Этого лучше избегать.
В интеграционных тестах важна «похожесть на настоящее», но это не значит, что каждый раз нужно бить в прод SaaS или прод API. Фаулер тоже советует поднимать внешние сервисы локально, ставить fake или пользоваться выделенным test instance.
На практике удобно сочетать:
- локальную БД
- временные каталоги
- test host
- выделенную test environment
- fake service с зафиксированным контрактом
7. Рекомендуемая структура на практике
Абсолютно правильного соотношения нет. Но достаточно универсально работает такая трёхслойная схема.
| Слой | Основной инструмент | Что туда класть |
|---|---|---|
| Слой ядра | Толстый набор юнит-тестов | Бизнес-правила, переходы состояний, проверка ввода, классификация ошибок |
| Слой границ | Узкие интеграционные тесты | БД, файлы, HTTP, serializer, DI, конфигурация, COM, права доступа |
| Слой целого | Небольшое число smoke / E2E | Проверка запуска, основные потоки, чтобы серьёзный сбой не повторился |
Интуитивно: плотнее числом становятся юнит-тесты, плотнее покрытием границ — интеграционные.
Рекомендуемый порядок такой.
- Сначала перечислить границы приложения
- Привести логику к виду, который можно отделить от внешнего мира
- На каждой границе поставить «минимум один happy path» и «типичный failure path»
- Сквозные прогоны целого сузить по числу
- Когда вылез баг, добавить тест на тот слой, где этот баг воспроизводится с наименьшими затратами
Пятый пункт — самый важный.
- Ошибка правила — добавьте юнит-тест
- Ошибка SQL / binding / конфигурации / прав доступа / регистрации — добавьте интеграционный тест
- Сбой, в котором участвуют запуск или распространение приложения, — добавьте smoke или E2E
При таком росте обязанности тестов меньше плывут.
8. Пять вопросов на случай сомнений
В конце — пять вопросов для проверки, когда неясно.
- Если подставить in-memory fake, смысл, который хотели проверить, остаётся?
- Если остаётся — это ближе к юнит-тесту.
- Если что-то сломается, подозревать будете скорее соединение или настройку, а не логику?
- Если да — это ближе к интеграционному тесту.
- Предмет проверки — не БД / файлы / serializer / DI / route / model binding / ОС / права доступа / bitness / thread?
- Если да — это ближе к интеграционному тесту.
- Нужно быстро прогнать много входных вариантов?
- Если да — это ближе к юнит-тесту.
- Когда этот тест падает, сразу ли понятно, что чинить?
- Если нет — слои тестов смешаны.
Разобравшись по этим пяти вопросам, легче не принимать небрежные решения вроде «интеграционный, потому что как-то ближе к реальности» или «юнит, потому что как-то быстрее».
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 ↩ ↩2
-
Martin Fowler, Mocks Aren’t Stubs ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как ускорить проверку приложений в Windows Sandbox
Разбираем, как с помощью Windows Sandbox быстрее локализовать проблемы с правами администратора, воспроизводить сценарии в чистом окружен...
Минимальные требования к самописному логгеру и чек-лист интеграционных тестов
Чтобы диагностические логи собственного приложения заслуживали доверия, разбираем формат UTF-8 JSON Lines, обязательные поля, flush, рота...
Как в Windows-приложении вынести только операции, которым нужны права администратора
Разбираем, как оставить UI Windows-приложения asInvoker и вынести операции с правами администратора в helper EXE: UAC, runas, именованные...
Хранение секретов в Windows-приложениях — избегаем настроек в открытом виде с помощью DPAPI
Чтобы не хранить сведения о подключении и API-токены в конфигурационных файлах Windows-приложений открытым текстом, разбираем подход DPAP...
Минимальный чек-лист безопасности при разработке Windows-приложений
Чек-лист базовых мер безопасности для бизнес-приложений на WPF / WinForms / WinUI / C++ / C#: права, подпись кода, обновления, секреты, H...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Технические консультации и ревью дизайна
Где именно резать границу между юнит-тестами и интеграционными тестами, удобно разбирать как ревью проектирования до реализации или как консультацию по тестовой стратегии.
Разработка приложений для Windows
В Windows-приложениях границы вроде файлов, прав доступа, COM, 32-bit / 64-bit напрямую ложатся и на слои тестов, поэтому тема хорошо стыкуется с выбором подхода к реализации.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Как разделить юнит-тесты и интеграционные тесты?
- Одной фразой: юнит-тесты — это тесты решений, интеграционные тесты — тесты соединений. Расчёт суммы, переходы состояний, проверка ввода, классификация исключений — то, чей смысл завершается без внешних ресурсов, — сдвигайте в юнит-тесты. То, что «подводит в момент соединения» — выполнение SQL, сериализация JSON/CSV, маршрутизация, регистрация в DI, файловые блокировки, права доступа, регистрация COM, 32-bit/64-bit — держите на стороне интеграционных тестов. Если проверить можно и там, и там, начинайте с юнит-теста.
- Что стоит поднимать в интеграционные тесты?
- В целом это четыре границы: формат, связка, среда и время. Формат — JSON/CSV, mapping БД, кодировки. Связка — как реальные части соединены: регистрация в DI, маршрутизация, привязка модели. Среда — реальное поведение ОС: права на файлы, регистрация COM, 32-bit/64-bit, STA/MTA. Время — timeout, cancellation, race condition. На in-memory fake смысл этих проверок сильно падает, поэтому безопаснее закрывать их интеграционными тестами.
- Почему плохо, если в юнит-тесте слишком много mock-ов?
- Если нужно 7 mock-ов, setup длинный, непонятно, что вообще хотели проверить, — либо у класса слишком много обязанностей, либо в юнит-тест затолкнули связку, которую на самом деле должен проверять интеграционный тест. Mock — инструмент, чтобы отсечь внешний мир, а не доказательство, что соединение с настоящим компонентом работает. Если это перепутать, легко получить «всё green, а в проде падает».
- Какую структуру и соотношение тестов держать?
- Универсально работает трёхслойная схема. Слой ядра держит бизнес-правила и переходы состояний толстым набором юнит-тестов; слой границ — узкие интеграционные тесты на БД, файлы, serializer, DI; слой целого — запуск и основные потоки небольшим числом smoke/E2E. Полный перебор ветвлений гоняйте юнит-тестами; интеграционные тесты сужайте до минимум одного happy path и типичного failure path на каждой границе. Когда вылез баг, тест добавляйте на тот слой, где его дешевле всего воспроизвести.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.