Тестирование
Уровни тестов, модель xUnit, моки и их злоупотребление, интеграционные тесты через WebApplicationFactory, тестируемое время и о чём молчит покрытие.
13 вопросов
JuniorТеорияОчень частоЧто такое Arrange-Act-Assert, и что относится к каждой из трёх его секций?
Что такое Arrange-Act-Assert, и что относится к каждой из трёх его секций?
Он делит тест на три блока: Arrange готовит входные данные и проверяемый объект, Act вызывает единственную тестируемую операцию, а Assert проверяет наблюдаемый результат. Один Act на тест означает, что падение указывает ровно на одно поведение.
Типичные ошибки
- ✗Втискивать несколько вызовов Act в один тест, из-за чего падение перестаёт называть одно поведение
- ✗Ставить проверки внутрь блока Arrange, размывая границу между подготовкой и верификацией
- ✗Считать Arrange-Act-Assert возможностью фреймворка, а не соглашением о структуре тела теста
Уточняющие вопросы
- →Как формулировка
Given-When-Thenиз behaviour-driven тестирования ложится на Arrange-Act-Assert? - →Когда допустимо, чтобы один тест нёс больше одной проверки?
JuniorТеорияОчень частоЧем модульный тест отличается от интеграционного и от end-to-end теста?
Чем модульный тест отличается от интеграционного и от end-to-end теста?
Модульный тест проверяет один класс в изоляции, с подменёнными зависимостями. Интеграционный запускает настоящие компоненты вместе — код плюс реальную базу или HTTP-конвейер. End-to-end гоняет развёрнутую систему снаружи. Стоимость растёт с каждым уровнем.
Типичные ошибки
- ✗Называть модульным тест, который ходит в настоящую базу данных
- ✗Считать уровни взаимозаменяемыми, а не различными по охвату и стоимости
- ✗Строить набор преимущественно из end-to-end тестов, потому что они кажутся реалистичнее
Уточняющие вопросы
- →Почему пирамида тестов помещает большинство тестов на модульный уровень?
- →Куда отнести тест, который ходит в реальную базу, но не поднимает HTTP-слой?
JuniorТеорияЧастоКогда в тест-фреймворке xUnit пишут [Fact], а когда [Theory]?
Когда в тест-фреймворке xUnit пишут [Fact], а когда [Theory]?
[Fact] помечает тест без аргументов — единственный сценарий, который должен выполняться. [Theory] помечает параметризованный тест: он прогоняется по разу на каждый набор данных из [InlineData] или [MemberData], и каждый набор отчитывается как отдельный тест.
Типичные ошибки
- ✗Ставить параметры метода на
[Fact], чтоxUnitотвергает ещё при обнаружении теста - ✗Считать, что все строки
[InlineData]схлопываются в один отчётный тест, а не в тест на набор - ✗Тянуться за
[Theory], когда сценарии различаются поведением, а не только входными данными
Уточняющие вопросы
- →Когда
[MemberData]предпочтительнее длинного списка строк[InlineData]? - →Как сохранить читаемость
[Theory], когда каждому набору нужен сложный объект?
JuniorТеорияЧастоЧем отличаются тест-фреймворки xUnit, NUnit и MSTest, и почему xUnit пересоздаёт класс теста на каждый тест?
Чем отличаются тест-фреймворки xUnit, NUnit и MSTest, и почему xUnit пересоздаёт класс теста на каждый тест?
Все три находят тесты по атрибутам и отчитываются о результатах, различаясь стилем. NUnit и MSTest переиспользуют один экземпляр класса с [SetUp]/[TearDown]. xUnit создаёт свежий экземпляр на каждый тест — конструктор играет роль настройки, — и состояние не протекает.
Типичные ошибки
- ✗Думать, что
xUnitпереиспользует один экземпляр класса теста на все свои тесты - ✗Искать
[SetUp]/[TearDown]вxUnit— эти роли играют конструктор иIDisposable - ✗Опираться на порядок выполнения или на состояние, оставленное более ранним тестом
Уточняющие вопросы
- →Как интерфейс общего контекста
IClassFixture<T>переиспользует дорогую настройку, не разделяя изменяемое состояние? - →Что заменяет
[TearDown]вxUnit, когда тесту нужна уборка за собой?
MiddleТеорияЧастоПочему антипаттерн злоупотребления моками делает набор тестов хрупким, и что стоит мокать вместо этого?
Почему антипаттерн злоупотребления моками делает набор тестов хрупким, и что стоит мокать вместо этого?
Когда мокается каждая зависимость, тест проверяет взаимодействия — какие вызовы были и в каком порядке, — то есть закрепляет реализацию, а не поведение: он переживает неверную логику, но ломается от рефакторинга. Мокайте границы, которыми вы не владеете; внутри берите fake.
Типичные ошибки
- ✗Проверять число вызовов через
Verify, когда важен результат, который наблюдает вызывающий код - ✗Мокать объекты-значения и внутрипроцессные зависимости вместо одних лишь чужих границ
- ✗Читать зелёный перемоканный набор как доказательство того, что поведение верно
Уточняющие вопросы
- →Когда рукописный in-memory fake предпочтительнее мока на
Moq? - →Как протестировать метод, у которого единственный наблюдаемый эффект — вызов внешнего шлюза?
SeniorДизайнЧастоУ вас сервис на ASP.NET Core, чей слой репозиториев работает через EF Core с PostgreSQL. Он опирается на уникальные ограничения, каскадные удаления, денежную колонку decimal и пару рукописных SQL-проекций. Команда хочет быстрый и надёжный набор тестов для этого слоя и выбирает из трёх вариантов: in-memory провайдер EF Core, SQLite in-memory и настоящий PostgreSQL, поднимаемый на прогон библиотекой одноразовых контейнеров Testcontainers. Спроектируйте стратегию. Скажите, что каждый вариант доказывает, а что нет, где in-memory провайдер EF Core даёт ложную уверенность, как вы будете засевать и изолировать данные между тестами и что всё равно останется модульному тесту вообще без базы.
У вас сервис на ASP.NET Core, чей слой репозиториев работает через EF Core с PostgreSQL. Он опирается на уникальные ограничения, каскадные удаления, денежную колонку decimal и пару рукописных SQL-проекций. Команда хочет быстрый и надёжный набор тестов для этого слоя и выбирает из трёх вариантов: in-memory провайдер EF Core, SQLite in-memory и настоящий PostgreSQL, поднимаемый на прогон библиотекой одноразовых контейнеров Testcontainers. Спроектируйте стратегию. Скажите, что каждый вариант доказывает, а что нет, где in-memory провайдер EF Core даёт ложную уверенность, как вы будете засевать и изолировать данные между тестами и что всё равно останется модульному тесту вообще без базы.
In-memory провайдер EF Core — это LINQ по объектам: ни ограничений, ни каскадных удалений, ни трансляции в SQL — он пропускает запросы, которые настоящая база отвергнет. SQLite даёт настоящий SQL на другом диалекте; маппинг проверяет только Testcontainers. Засевайте внутри откатываемой транзакции, а чистые правила оставьте модульным тестам без базы.
Типичные ошибки
- ✗Доверять in-memory провайдеру EF Core, который не соблюдает ограничений и не транслирует SQL
- ✗Мокать
DbContextиDbSet<T>, проверяя LINQ по списку вместо трансляции ваших запросов - ✗Делить одну базу между тестами без сброса, из-за чего результат зависит от порядка выполнения
Уточняющие вопросы
- →Как удержать прогон PostgreSQL в
Testcontainersдостаточно быстрым для набора перед коммитом? - →Какие дефекты SQLite in-memory всё равно скроет, а вскроет только настоящая СУБД?
MiddleТеорияИногдаЧто измеряет line-покрытие, и чем от него отличается branch-покрытие?
Что измеряет line-покрытие, и чем от него отличается branch-покрытие?
Line-покрытие — доля исполняемых строк, которых коснулся прогон; branch-покрытие — доля пройденных исходов условий. Один тест сквозь if (a && b) задевает каждую строку, оставляя большинство комбинаций ветвей неопробованными.
Типичные ошибки
- ✗Читать покрытую строку как проверенную, тогда как покрытие фиксирует исполнение, а не проверку
- ✗Считать, что полное line-покрытие означает, что каждый исход условия был опробован
- ✗Принимать 100% branch-покрытия за доказательство, что каждый путь исполнения был пройден
Уточняющие вопросы
- →Как
[Theory]с удачно подобранными наборами поднимает branch-покрытие там, где один[Fact]не может? - →Почему тест вообще без единой проверки всё равно способен догнать покрытие до 100%?
MiddleТеорияИногдаКакую задачу решают шаблоны подготовки данных Test Data Builder и Object Mother?
Какую задачу решают шаблоны подготовки данных Test Data Builder и Object Mother?
Оба держат Arrange коротким, когда валидному объекту нужно много полей. Object Mother отдаёт именованные канонические экземпляры; builder цепочкой fluent-сеттеров настраивает значения по умолчанию, так что тест указывает лишь важное ему поле. Новое поле ломает одно место.
Типичные ошибки
- ✗Копировать двадцатистрочную сборку объекта в каждый тест вместо того, чтобы вынести её в одно место
- ✗Позволять Object Mother отдавать один общий изменяемый экземпляр, который тесты затем меняют
- ✗Нагружать builder таким числом опций, что тест читается хуже, чем прямая сборка объекта
Уточняющие вопросы
- →Как builder сохраняет читаемость наборов данных
[Theory], когда каждому набору нужен сложный объект? - →Когда фиксированный набор именованных экземпляров Object Mother становится обузой в поддержке?
MiddleТеорияИногдаЧто такое snapshot-тестирование, и когда оно уместнее явных проверок?
Что такое snapshot-тестирование, и когда оно уместнее явных проверок?
Оно сериализует результат, сравнивает его с утверждённым файлом в репозитории и падает на любом расхождении; вы читаете diff и осознанно переутверждаете. Оно годится для крупного стабильного вывода вроде ответа API и неуместно там, где вывод часто меняется или недетерминирован.
Типичные ошибки
- ✗Переутверждать упавший snapshot, не читая diff, чем тихо стирается регрессия
- ✗Снимать snapshot с вывода, где зашиты метка времени, GUID или порядок словаря, делая тест флаки
- ✗Тянуться за snapshot там, где одна явная проверка ясно выразила бы намерение
Уточняющие вопросы
- →Как сохранить стабильность snapshot, когда в полезной нагрузке есть сгенерированные id или метки времени?
- →Как развести по snapshot на набор в
[Theory]с несколькими строками данных?
MiddleТеорияИногдаКак тестировать логику с DateTime.Now, не замораживая часы глобально?
Как тестировать логику с DateTime.Now, не замораживая часы глобально?
Перестаньте звать окружающие часы — внедрите абстракцию времени и пусть код спрашивает её. В .NET 8+ это TimeProvider: в бою TimeProvider.System, в тесте FakeTimeProvider. Глобальная подмена протекает между тестами и убивает параллельный прогон.
Типичные ошибки
- ✗Подменять
DateTime.Nowизменяемой статикой, которая протекает состоянием между параллельными тестами - ✗Верить, что
Moqспособен перехватить статическое свойство вродеDateTime.Now - ✗Добавлять
Thread.Sleepи допуск в проверке вместо того, чтобы взять время под контроль
Уточняющие вопросы
- →Как
FakeTimeProvider.Advanceпозволяет проверить таймаут, не дожидаясь его на самом деле? - →Что меняется, когда код планирует работу через
Task.Delay, а не читает часы?
MiddleТеорияИногдаКак интеграционный хост WebApplicationFactory<T> позволяет тестировать приложение ASP.NET Core?
Как интеграционный хост WebApplicationFactory<T> позволяет тестировать приложение ASP.NET Core?
Он поднимает ваш настоящий конвейер Program внутри процесса — реальные middleware, маршрутизацию, фильтры, привязку модели — и отдаёт HttpClient на in-memory TestServer, без сокета. Переопределение регистраций в DI позволяет подменить базу.
Типичные ошибки
- ✗Думать, что
WebApplicationFactory<T>открывает настоящий порт, а не поднимает in-memoryTestServer - ✗Регистрировать тестовый дубль до собственной регистрации приложения, из-за чего настоящая перезаписывает его
- ✗Называть такой тест end-to-end, когда браузера, сети и развёртывания в нём нет вовсе
Уточняющие вопросы
- →Как подменить настоящую регистрацию
DbContextтестовой внутриConfigureWebHost? - →Почему прошедший тест на
WebApplicationFactory<T>всё ещё не доказывает, что развёртывание работает?
SeniorДизайнИногдаВаш набор в CI падает примерно на одном прогоне из двадцати, каждый раз в другом тесте, и любое падение проходит при перезапуске; привычка команды — жать Retry. Набор смешивает модульные тесты, интеграционные на хосте WebApplicationFactory<T> против общей базы и несколько snapshot-тестов над сериализованными ответами API, а прогон идёт параллельно по классам. Объясните, что системно делает набор недетерминированным, как вы найдёте здесь настоящие источники, а не будете гадать, и как заложите детерминизм — с учётом окружающего времени, состояния, общего для параллельных тестов, допущений о порядке и недетерминированных частей сериализованного snapshot. Скажите также, какую политику вы установите для теста, который остаётся флаки.
Ваш набор в CI падает примерно на одном прогоне из двадцати, каждый раз в другом тесте, и любое падение проходит при перезапуске; привычка команды — жать Retry. Набор смешивает модульные тесты, интеграционные на хосте WebApplicationFactory<T> против общей базы и несколько snapshot-тестов над сериализованными ответами API, а прогон идёт параллельно по классам. Объясните, что системно делает набор недетерминированным, как вы найдёте здесь настоящие источники, а не будете гадать, и как заложите детерминизм — с учётом окружающего времени, состояния, общего для параллельных тестов, допущений о порядке и недетерминированных частей сериализованного snapshot. Скажите также, какую политику вы установите для теста, который остаётся флаки.
Флакинес — это общее изменяемое состояние при параллельном прогоне плюс недетерминизм окружения: настоящие часы, общая на классы база, тесты на чужих остатках, snapshot с метками времени. Внедрите TimeProvider, изолируйте тест откатываемой транзакцией, вычищайте изменчивые поля — и отправляйте в карантин, а не в Retry, то, что всё ещё флакает.
Типичные ошибки
- ✗Перезапускать флаки-тест в CI, пряча настоящую гонку или зависимость от порядка за зелёным прогоном
- ✗Винить раннер, пока параллельные классы тестов делят одну базу или статический кэш
- ✗Снимать snapshot с нагрузки, где зашиты метка времени или GUID, и каждый раз переутверждать расхождение
Уточняющие вопросы
- →Как доказать, что конкретный тест зависит от порядка, а не от времени?
- →Какую изоляцию вы дадите каждому параллельному классу тестов против одного общего PostgreSQL?
SeniorТеорияРедкоПочему 100% покрытия не доказывает качество, и что измеряет техника внесения дефектов mutation testing?
Почему 100% покрытия не доказывает качество, и что измеряет техника внесения дефектов mutation testing?
Покрытие фиксирует, что строка исполнилась, а не что её кто-то проверил: удалите все проверки — оно всё равно покажет 100%. Mutation testing вместо этого вносит мелкие дефекты и заново гоняет набор: выживший мутант — код, который тесты исполняют, но не проверяют.
Типичные ошибки
- ✗Считать покрытую строку проверенной, тогда как набор вообще без проверок всё равно покажет 100%
- ✗Читать выжившего мутанта как слишком строгий тест, а не как непроверенное поведение
- ✗Ставить порог покрытия как планку качества, которую перемоканные тесты тихо перешагивают
Уточняющие вопросы
- →Почему долю убитых мутантов дорого считать, и как ограничить такой прогон в CI?
- →Каких мутантов стоит списать как эквивалентные, и кто это решает?