Тестируемый код: DI, TimeProvider, границы
↑ 2.4 Тестирование в .NET — основы · ← Предыдущая · → Следующая
Зачем это на собесе
Проверка проектного мышления: почему код «не тестируется» и как это исправить не моками, а дизайном.
Объяснение
Код плохо тестируется, когда логика смешана с побочными эффектами и скрытыми зависимостями: DateTime.Now, new HttpClient(), статика, синглтоны, Random.
| Проблема | Решение |
|---|---|
| Текущее время | TimeProvider (.NET 8) + FakeTimeProvider |
| Случайность и Guid | внедрять источник или seed |
| Файловая система, сеть | абстракция на границе или реальная зависимость в интеграционном тесте |
| Статические вызовы | обёртка или перенос в композиционный корень |
| Бизнес-логика вместе с I/O | «функциональное ядро, императивная оболочка» |
public class TokenService(TimeProvider clock)
{
public bool IsExpired(Token t) => t.ExpiresAt <= clock.GetUtcNow();
}
// в тесте
var time = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var sut = new TokenService(time);
time.Advance(TimeSpan.FromHours(2));
Assert.True(sut.IsExpired(token));Функциональное ядро
Чистая логика принимает данные и возвращает решение, оболочка читает и пишет данные. Ядро тестируется без моков.
// чистая функция
static Decision Evaluate(Order o, DateTimeOffset now) => /* ... */;
// оболочка
public async Task HandleAsync(Guid id, CancellationToken ct)
{
var order = await repo.GetAsync(id, ct);
var decision = Evaluate(order, clock.GetUtcNow());
await repo.SaveAsync(decision, ct);
}Нюансы и подводные камни
- Конструктор с 10 зависимостями — признак нарушения SRP, а не проблемы тестов.
- Не выносите в интерфейс всё подряд: интерфейс ради мока без второй реализации лишний.
- Service locator и статический доступ к контейнеру скрывают зависимости.
internalчлены можно открыть тестам черезInternalsVisibleTo, но лучше тестировать публичное поведение.
Практика
- Замените
DateTime.UtcNowв проекте наTimeProvider. - Выделите чистую функцию из метода с I/O и покройте её табличными тестами.
- Найдите класс с максимальным числом зависимостей и разбейте.
Вопросы с ответами
Как протестировать код, зависящий от времени?
Внедрить
TimeProviderи использоватьFakeTimeProvider, управляя временем в тесте.
Что такое композиционный корень?
Единственное место (обычно
Program.cs), где собирается граф зависимостей; остальной код получает зависимости через конструктор.
Сколько интерфейсов нужно для тестируемости?
Столько, сколько границ с внешним миром; интерфейс на каждый класс — избыточен.