---
course: ПиТПМ
lecture: 3
title: "Лекция №3. Архитектура тестируемых систем. Изоляция компонентов. Использование mock-объектов и заглушек (Stubs, Mocks)"
---

# ◆ Лекция №3. Архитектура тестируемых систем. Изоляция компонентов. Использование mock-объектов и заглушек (Stubs, Mocks)

Введение

Во второй лекции мы освоили модульное тестирование как метод проверки отдельных модулей кода. Мы научились писать тесты по схеме AAA, измерять покрытие и понимать, какие участки кода остаются непроверенными. Однако мы столкнулись с **фундаментальной проблемой**: не любой код можно легко протестировать.

## §3.1 От юнит-тестов к архитектуре: почему тесты требуют правильного проектирования

В реальных проектах метод редко существует в вакууме. Он почти всегда обращается к внешним ресурсам — базам данных, файловой системе, веб-сервисам, сторонним API, очередям сообщений. Если мы попытаемся запустить юнит-тест, который обращается к реальной базе данных, мы получим три негативных эффекта:

1. **Медленный тест**

   — сетевые вызовы и дисковые операции занимают секунды, а не миллисекунды. Набор из сотни таких тестов сделает сборку проекта невыносимо долгой.
2. **Нестабильный тест**

   — если база данных недоступна, изменились тестовые данные или сетевой таймаут превышен, тест упадет без вины разработчика. Такой тест называют **flaky** (нестабильным).
3. **Сложный тест**

   — требуется настраивать окружение, создавать тестовые данные, чистить их после прогона. Это увеличивает время на написание и поддержку тестов.

Рассмотрим пример нетестируемого кода:

csharp

```csharp
public class OrderService 
{ 
    private readonly SmtpClient _smtpClient = new SmtpClient("smtp.gmail.com");
    
    public void ProcessOrder(Order order) 
    {
        // Бизнес-логика обработки заказа...
        _smtpClient.Send(order.CustomerEmail, "Заказ принят", "Спасибо за покупку!"); 
    } 
}
```

Проблема

Этот код невозможно протестировать изолированно. Причина — жесткая зависимость от конкретной реализации `SmtpClient`. Класс сам создает свою зависимость, не позволяя подменить её в тесте.

Из этой ситуации следует **ключевой принцип модульного тестирования**:

Юнит-тест должен проверять логику, а не интеграцию

Если тест обращается к внешнему ресурсу — это уже не юнит-тест, а интеграционный. Интеграционные тесты тоже нужны, но у них другая скорость, другая цена и другая область применения.

Таким образом, **проблема тестируемости — это не проблема тестов. Это проблема архитектуры кода.**

Когда мы говорим «код плохо тестируется», мы на самом деле говорим «код плохо спроектирован». И исправлять это нужно не дописыванием тестов, а изменением архитектуры.

Именно этим мы и займемся в данной лекции. Мы поднимемся на уровень выше: от написания тестов — к проектированию систем, которые можно тестировать. Мы разберем:

- **Test Doubles** (Stubs и Mocks) — объекты-заменители, которые позволяют изолировать тестируемый модуль.
- **Dependency Injection** (DI) — паттерн, при котором зависимости передаются в класс извне, а не создаются внутри.
- **Dependency Inversion Principle** (DIP) — принцип SOLID, требующий зависимости от абстракций, а не от конкретных реализаций.
- **IoC-контейнеры** — инструменты, автоматизирующие управление зависимостями в больших проектах.
- **Антипаттерны** — частые ошибки проектирования, которые делают код нетестируемым, и способы их исправления.

Важность принципов

Эти принципы превращают тестирование из «проверки написанного кода» в инструмент проектирования архитектуры. Если систему трудно тестировать — это сигнал, что архитектура нуждается в улучшении.

Владение этими инструментами отличает инженера по качеству от «нажимателя кнопок» и позволяет участвовать в обсуждении архитектурных решений наравне с разработчиками.

## §3.2 Проблема зависимостей: что делать, когда модуль не изолирован

В современной разработке тестирование перестало быть второстепенной задачей, выполняемой после написания кода. Сегодня оно становится неотъемлемой частью процесса проектирования, а его невозможность — верным признаком архитектурных просчетов. Чтобы наглядно продемонстрировать это утверждение, обратимся к конкретному примеру из реальной практики.

Предположим, мы создаем систему интернет-магазина и сталкиваемся с задачей генерации отчета о продажах — на первый взгляд, тривиальной и не требующей особых размышлений. Однако именно на таком, казалось бы, простом фрагменте кода мы обнаруживаем целый букет проблем, которые делают тестирование не просто сложным, а практически невозможным без доработки самого дизайна.

Рассмотрим этот пример внимательно, чтобы понять, как смешение ответственностей и жесткие зависимости от внешних ресурсов превращают рабочий код в архитектурную ловушку.

csharp

```csharp
public class ReportGenerator 
{ 
    public void GenerateSalesReport(DateTime start, DateTime end) 
    { 
        // 1. Получение данных из базы 
        var connection = new SqlConnection("Server=localhost;Database=Shop;"); 
        connection.Open(); 
        var command = new SqlCommand(
            "SELECT SUM(Total) FROM Orders WHERE OrderDate BETWEEN @start AND @end", 
            connection 
        ); 
        var total = command.ExecuteScalar();

        // 2. Формирование отчета 
        var report = $"Отчет за период: {start:dd.MM.yyyy}-{end:dd.MM.yyyy}\n"; 
        report += $"Итого продаж: {total} руб.";

        // 3. Сохранение в файл 
        File.WriteAllText($"report_{start:yyyyMMdd}_{end:yyyyMMdd}.txt", report); 
    } 
}
```

Три фатальные проблемы

На первый взгляд код рабочий. Но с точки зрения тестирования он содержит три фатальные проблемы.

**Первая проблема — жесткая связь с базой данных.** Класс сам создает подключение и формирует SQL-запрос. В тесте мы не можем подменить БД на тестовую или заглушку. Это означает, что для проверки метода нам каждый раз потребуется реальная база данных с реальными данными.

**Вторая проблема — работа с файловой системой.** Метод сохраняет отчет на диск. В тесте мы не можем проверить, какой именно файл создан и что в нем записано, не заглядывая в папку. Более того, если несколько тестов запускаются параллельно, они могут перезаписать файлы друг друга.

**Третья проблема — смешение ответственностей.** Один метод делает три разных дела: получает данные, формирует отчет, сохраняет файл. Это нарушает **принцип единственной ответственности** (Single Responsibility Principle). Метод сложно тестировать, потому что нужно проверять все три аспекта одновременно.

Что мы хотим от теста для этого метода? Мы хотим проверить:

1. Что SQL-запрос формируется корректно (с правильными параметрами).
2. Что отчет формируется в правильном формате (заголовок, итоговая сумма).
3. Что файл сохраняется с правильным именем и содержимым.

Но мы **не хотим**, чтобы тест:

- Обращался к реальной базе данных.
- Создавал реальные файлы на диске.
- Зависел от настроек окружения.

Ключевое противоречие

Это ключевое противоречие: логика и внешние ресурсы смешаны в одном методе. Их нужно разделить. Тестируемая логика должна быть изолирована от внешнего мира.

Тестировщик, видя такой код, должен понимать: его задача — не просто написать тесты к этому методу (это будет сложно и дорого), а вернуть его разработчику с комментарием:

Профессиональный подход

«Метод не тестируем. Требуется рефакторинг: вынести работу с БД и файловой системой в отдельные компоненты и передавать их через конструктор».

Именно так выглядит профессиональный подход: тестировщик не просто находит дефекты в коде, а участвует в проектировании, указывая на архитектурные изъяны, которые делают тестирование невозможным. Найденный на этапе написания кода архитектурный дефект стоит в сотни раз дешевле, чем тот же дефект, обнаруженный в продакшене.

Вывод

**Проблема изоляции модуля — это не техническая деталь, а архитектурное требование.** В следующих параграфах мы разберем, как правильно проектировать код, чтобы он был изолирован и тестируем: через Test Doubles, Dependency Injection и соблюдение принципов SOLID.

## §3.3 Введение в тест-дублёры (Test Doubles): что это и зачем нужно

В предыдущем параграфе мы столкнулись с проблемой: код класса `ReportGenerator` жестко привязан к базе данных и файловой системе. Чтобы протестировать такой код, нам нужно чем-то заменить эти внешние зависимости на управляемые объекты. Для этого в инженерии тестирования используется понятие **Test Double** (тест-дублёр).

Термин был введён Джерардом Месзаросом и происходит из кинематографа: когда актёр не может выполнить опасный трюк, снимается дублёр. В тестировании дублёр заменяет реальный компонент системы, чтобы мы могли контролировать тестовую среду и проверять конкретные сценарии.

Существует пять основных видов Test Doubles, каждый из которых решает свою задачу:

| Вид | Назначение | Когда использовать |
| --- | --- | --- |
| **Dummy** | Объект, который передаётся, но никогда не используется. Нужен только для удовлетворения сигнатуры метода. | Когда параметр обязателен, но не влияет на тест. |
| **Stub** | Предоставляет фиксированные ответы на вызовы методов. | Когда нужно вернуть определённое значение (например, курс валют, текущую дату). |
| **Spy** | Аналог Stub, но дополнительно запоминает, как его вызывали (сколько раз, с какими параметрами). | Когда нужно проверить, что метод был вызван, но без строгих ожиданий. |
| **Mock** | Заранее настроен на ожидание конкретных вызовов. Тест проверяет, были ли они выполнены корректно. | Когда нужно проверить взаимодействие (отправка письма, логирование, сохранение в БД). |
| **Fake** | Упрощённая рабочая реализация (например, база данных в памяти вместо реальной). | Когда нужна полноценная работа, но без внешних ресурсов. |

В экосистеме .NET для создания Stub и Mock чаще всего используются библиотеки **Moq** и **NSubstitute**. Именно они являются индустриальным стандартом и будут применяться в наших примерах.

Фундаментальное различие Stub и Mock

- **Stub** отвечает на вопрос: «Что вернуть?». Его задача — поставить данные, чтобы тест мог выполниться. Stub не проверяет, как его вызывали.
- **Mock** отвечает на вопрос: «Как вызывали?». Его задача — убедиться, что метод был вызван с правильными параметрами, нужное количество раз. Mock проверяет взаимодействие.

Эта разница не просто терминологическая. Она определяет стратегию тестирования.

Если мы проверяем результат вычислений (например, конвертацию валюты) — нам нужен **Stub**, который вернет курс. Если мы проверяем побочный эффект (отправлено ли письмо) — нам нужен **Mock**, который подтвердит факт вызова.

Хотя на практике в .NET-проектах чаще всего используются Stub и Mock, понимание остальных видов Test Doubles помогает читать профессиональную литературу и участвовать в архитектурных обсуждениях. Рассмотрим их кратко.

## §3.4 Stubs (Заглушки) — поставщики фиксированных ответов

**Stub**

— это вид тест-дублёра, который возвращает заранее заданные данные при вызове своих методов.

Его задача — не проверять взаимодействие, а поставлять данные, чтобы тестируемая логика могла выполниться. Stub не заботится о том, как его вызывали, сколько раз и в каком порядке. Он просто отвечает на вызов фиксированным результатом.

Рассмотрим практический пример. Пусть у нас есть сервис конвертации валют, который для расчета использует внешний API курсов:

csharp

```csharp
public interface IExchangeRateProvider 
{ 
    decimal GetRate(string fromCurrency, string toCurrency); 
}

public class CurrencyConverter 
{ 
    private readonly IExchangeRateProvider _rateProvider;

    public CurrencyConverter(IExchangeRateProvider rateProvider) 
    { 
        _rateProvider = rateProvider; 
    }

    public decimal Convert(decimal amount, string from, string to) 
    { 
        var rate = _rateProvider.GetRate(from, to); 
        return amount * rate; 
    } 
}
```

В реальной работе `GetRate` делает HTTP-запрос к внешнему сервису. Для юнит-теста нам это не нужно — мы не хотим зависеть от сети, скорости ответа и доступности API. Вместо этого мы создаём Stub, который возвращает фиксированный курс.

С использованием библиотеки Moq это делается следующим образом:

csharp

```csharp
[Fact] 
public void Convert_UsdToEur_ReturnsCorrectAmount() 
{ 
    // Arrange: создаём Stub 
    var stubRateProvider = new Mock<IExchangeRateProvider>();
    
    // Настраиваем: при вызове GetRate("USD","EUR") возвращать 0.85 
    stubRateProvider
        .Setup(provider => provider.GetRate("USD", "EUR"))
        .Returns(0.85m);

    var converter = new CurrencyConverter(stubRateProvider.Object);

    // Act 
    var result = converter.Convert(100m, "USD", "EUR");

    // Assert 
    Assert.Equal(85m, result); 
}
```

Ключевая деталь

Обратите внимание на ключевую деталь: в этом тесте мы **не проверяем**, что метод `GetRate` был вызван. Нам не важно, был ли он вызван один раз или десять. Мы проверяем только результат вычисления — состояние объекта после выполнения метода.

Именно это и отличает Stub от других видов тест-дублёров, которые используются в тестировании.

Stub не обязан создаваться через библиотеку Moq. Moq — это просто удобный инструмент. Вместо него можно написать ручную реализацию интерфейса:

csharp

```csharp
// Ручной Stub (без Moq) 
public class StubExchangeRateProvider : IExchangeRateProvider 
{ 
    public decimal GetRate(string fromCurrency, string toCurrency) 
    { 
        return 0.85m; // всегда возвращаем фиксированный курс 
    } 
}

// Использование в тесте 
var stub = new StubExchangeRateProvider(); 
var converter = new CurrencyConverter(stub);

var result = converter.Convert(100m, "USD", "EUR");

Assert.Equal(85m, result);
```

Типичные примеры использования Stub

Stub идеально подходит для ситуаций, когда тестируемый метод зависит от внешних данных, но логика проверки сводится к вычислению результата.

Типичные примеры: поставщик текущей даты и времени, генератор случайных чисел, внешний API с фиксированным ответом, настройки конфигурации.

## §3.5 Mocks (Имитации) — проверка взаимодействий

**Mock**

— это вид тест-дублёра, который не только поставляет данные, но и проверяет, что методы были вызваны корректно: с правильными параметрами, нужное количество раз, в правильном порядке. Если Stub отвечает на вопрос «Что вернуть?», то Mock отвечает на вопрос «Как вызывали?».

Рассмотрим пример. Пусть у нас есть сервис обработки заказов, который после успешного оформления отправляет клиенту письмо-подтверждение:

csharp

```csharp
public interface IEmailSender 
{ 
    void Send(string to, string subject, string body); 
}

public class OrderService 
{ 
    private readonly IEmailSender _emailSender;

    public OrderService(IEmailSender emailSender) 
    { 
        _emailSender = emailSender; 
    }

    public void ProcessOrder(Order order) 
    {
        // Бизнес-логика: проверка заказа, расчет суммы, сохранение...
        //...
        
        // Отправка подтверждения 
        _emailSender.Send(
            order.CustomerEmail, 
            "Заказ подтвержден", 
            $"Ваш заказ №{order.Id} принят. Спасибо за покупку!"
        ); 
    } 
}
```

В этом коде метод `ProcessOrder` ничего не возвращает (`void`). Единственный способ проверить его корректность — убедиться, что он вызвал `Send` с правильными параметрами. Для этого используется Mock.

**Создание Mock через Moq:**

csharp

```csharp
[Fact] 
public void ProcessOrder_ValidOrder_SendsConfirmationEmail() 
{ 
    // Arrange: создаём Mock 
    var mockEmailSender = new Mock<IEmailSender>(); 
    var service = new OrderService(mockEmailSender.Object); 
    var order = new Order 
    { 
        Id = 123, 
        CustomerEmail = "customer@example.com" 
    }; 
    
    // Act 
    service.ProcessOrder(order); 
    
    // Assert: проверяем, что Send был вызван 
    // ровно 1 раз с правильными параметрами 
    mockEmailSender.Verify(
        sender => sender.Send(
            "customer@example.com",
            "Заказ подтвержден",
            "Ваш заказ №123 принят. Спасибо за покупку!"
        ), 
        Times.Once
    ); 
}
```

Ключевое отличие от Stub

Обратите внимание на ключевое отличие от Stub: мы не просто настраиваем возвращаемое значение (здесь его вообще нет), а проверяем факт вызова через метод `Verify`. Это и есть суть Mock — проверка взаимодействия между объектами.

Mock особенно полезен в следующих ситуациях:

- Метод ничего не возвращает (`void`), но должен выполнить побочное действие (отправка письма, логирование, запись в БД).
- Важно проверить не только факт вызова, но и аргументы (например, правильное форматирование письма).
- Нужно убедиться, что метод не вызван при некорректных входных данных.

**Пример проверки «отрицательного» сценария:**

csharp

```csharp
[Fact] 
public void ProcessOrder_NegativeTotal_DoesNotSendEmail() 
{ 
    var mockEmailSender = new Mock<IEmailSender>(); 
    var service = new OrderService(mockEmailSender.Object); 
    var invalidOrder = new Order 
    { 
        Id = 999, 
        CustomerEmail = "test@test.com", 
        Total = -100 
    };

    // Act 
    // предположим, метод выбрасывает исключение 
    service.ProcessOrder(invalidOrder);

    // Assert: проверяем, что Send НЕ был вызван 
    mockEmailSender.Verify(
        sender => sender.Send(
            It.IsAny<string>(), 
            It.IsAny<string>(), 
            It.IsAny<string>()
        ), 
        Times.Never 
    ); 
}
```

Здесь `Times.Never` проверяет, что метод `Send` не вызывался ни разу — что корректно для невалидного заказа, который не должен отправлять подтверждение.

**Метод `It.IsAny<T>()`**

В примере выше мы использовали конструкцию:

csharp

```csharp
It.IsAny<string>()
```

Это специальный метод из библиотеки Moq, который означает «любое значение данного типа».

Вместо того чтобы указывать конкретный email, тему или текст письма, мы говорим: «Мне всё равно, что именно передавалось — проверяю только факт вызова».

Это особенно полезно в двух случаях:

1. **Когда точное значение не важно.** Например, мы хотим проверить, что письмо было отправлено, но не хотим жестко привязываться к тексту письма (который может измениться).
2. **Когда значение генерируется внутри метода.** Например, если письмо содержит текущую дату или номер заказа, мы не можем предсказать его заранее в тесте.

**Сравнение подходов:**

csharp

```csharp
// Жесткая проверка — важен конкретный email 
mockEmailSender.Verify(
    sender => sender.Send(
        "customer@example.com", 
        "Заказ подтвержден",
        "Текст..."
    ), 
    Times.Once 
);

// Гибкая проверка — важно только, что вызвали с любым email 
mockEmailSender.Verify(
    sender => sender.Send(
        It.IsAny<string>(), 
        "Заказ подтвержден",
        "Текст..."
    ), 
    Times.Once 
);

// Проверка только факта вызова — параметры вообще не важны 
mockEmailSender.Verify(
    sender => sender.Send(
        It.IsAny<string>(), 
        It.IsAny<string>(), 
        It.IsAny<string>()
    ), 
    Times.Never 
);
```

В последнем примере мы используем `It.IsAny<string>()` для всех трех параметров, потому что нам важно лишь отсутствие вызова, а не то, с какими параметрами он мог бы произойти.

**Другие полезные методы Moq для проверки параметров:**

| Метод | Значение |
| --- | --- |
| `It.IsAny<T>()` | Любое значение типа T |
| `It.IsNotNull<T>()` | Любое не-null значение |
| `It.IsInRange<T>(min, max)` | Значение в диапазоне |
| `It.Is<T>(x => x > 0)` | Значение, удовлетворяющее условию |

## §3.6 Stub vs Mock: в чем разница и когда что использовать

После того как мы разобрали по отдельности Stub и Mock, возникает закономерный вопрос: «Какой из них использовать в конкретной ситуации и чем они принципиально отличаются?». Ответ кроется в том, что именно мы проверяем в тесте.

**Ключевое различие** можно сформулировать так:

- **Stub проверяет состояние** — нас интересует результат вычисления, который вернул тестируемый метод.
- **Mock проверяет поведение** — нас интересует, были ли вызваны зависимые методы с правильными параметрами.

| Характеристика | Stub | Mock |
| --- | --- | --- |
| **Основная задача** | Поставлять данные для теста | Проверять взаимодействие между объектами |
| **Что проверяет тест** | Состояние (результат метода) | Поведение (вызовы методов) |
| **Типичный сценарий** | Метод возвращает значение | Метод void, но выполняет побочное действие |
| **Основной метод в Moq** | `.Setup().Returns()` | `.Verify()` |
| **Пример** | Получение курса валют, текущей даты | Отправка письма, логирование, сохранение в БД |

Правило выбора

Правило выбора предельно простое:

- Если тестируемый метод **возвращает значение** (не void) — используйте **Stub**. Вам нужно проверить, что результат вычисления корректен.
- Если тестируемый метод **ничего не возвращает** (void) — используйте **Mock**. Единственный способ проверить его работу — убедиться, что он вызвал нужные методы с правильными параметрами.

Однако на практике эти подходы часто комбинируются в одном тесте. Рассмотрим пример, где есть и возвращаемое значение, и побочный эффект:

csharp

```csharp
public class OrderProcessor 
{ 
    private readonly IPriceCalculator _calculator; 
    private readonly ILogger _logger;

    public OrderProcessor(IPriceCalculator calculator, ILogger logger) 
    { 
        _calculator = calculator; 
        _logger = logger; 
    }

    public decimal ProcessOrder(Order order) 
    { 
        // Расчет стоимости (возвращает значение) 
        var total = _calculator.Calculate(order);

        // Логирование (побочный эффект, void) 
        _logger.Log($"Заказ №{order.Id} обработан. Сумма: {total}");

        return total; 
    } 
}
```

**Тест, комбинирующий Stub и Mock:**

csharp

```csharp
[Fact] 
public void ProcessOrder_ValidOrder_CalculatesAndLogs() 
{ 
    // Arrange: Stub для данных 
    var stubCalculator = new Mock<IPriceCalculator>(); 
    stubCalculator
        .Setup(c => c.Calculate(It.IsAny<Order>()))
        .Returns(1500m); // Stub возвращает данные

    // Arrange: Mock для проверки взаимодействия 
    var mockLogger = new Mock<ILogger>(); 
    var processor = new OrderProcessor(
        stubCalculator.Object, 
        mockLogger.Object
    ); 
    var order = new Order { Id = 42 };

    // Act 
    var result = processor.ProcessOrder(order);

    // Assert: проверка состояния (Stub) 
    Assert.Equal(1500m, result);

    // Assert: проверка поведения (Mock) 
    mockLogger.Verify(
        l => l.Log("Заказ №42 обработан. Сумма: 1500"), 
        Times.Once
    ); 
}
```

Здесь мы одновременно:

1. Используем **Stub** (`stubCalculator`) — чтобы подменить расчет и вернуть фиксированную сумму.
2. Используем **Mock** (`mockLogger`) — чтобы проверить, что логирование было выполнено корректно.

Вывод

Выбор между Stub и Mock — это не вопрос «что лучше», а вопрос «что мы проверяем». Если в тесте важен результат — используйте Stub. Если важны побочные эффекты — используйте Mock. А в реальных проектах чаще всего вы будете использовать их вместе.

## §3.7 Внедрение зависимостей (Dependency Injection) как архитектурная основа тестируемости

В предыдущих параграфах мы научились создавать Stub и Mock, но остался открытым вопрос: «Как физически подставить эти объекты-заменители в тестируемый код?». Ответ — через **Внедрение зависимостей** (Dependency Injection, DI).

**DI**

— это паттерн проектирования, при котором объект получает свои зависимости извне, а не создает их самостоятельно. Вместо того чтобы внутри класса писать `new SmtpClient()`, мы передаем готовый экземпляр через конструктор, метод или свойство.

Рассмотрим наглядный пример.

**Код без DI (жесткая связь):**

csharp

```csharp
public class OrderService 
{ 
    private readonly SmtpClient _smtp = new SmtpClient("smtp.gmail.com");

    public void ProcessOrder(Order order) 
    {
        // Бизнес-логика...
        _smtp.Send(order.Email, "Заказ принят", "Спасибо!"); 
    } 
}
```

Проблема

Мы не можем подменить `_smtp` в тесте, потому что он создается внутри класса.

**Код с DI (через конструктор):**

csharp

```csharp
public interface IMessageSender 
{ 
    void Send(string to, string subject, string body); 
}

public class OrderService 
{ 
    private readonly IMessageSender _messageSender;

    // ↓ зависимость приходит извне 
    public OrderService(IMessageSender messageSender) 
    { 
        _messageSender = messageSender; 
    }

    public void ProcessOrder(Order order) 
    {
        // Бизнес-логика...
        _messageSender.Send(order.Email, "Заказ принят", "Спасибо!"); 
    } 
}
```

Теперь в тесте мы можем передать всё что угодно:

csharp

```csharp
var mockSender = new Mock<IMessageSender>(); 
// ↓ подстановка заглушки 
var service = new OrderService(mockSender.Object);
```

Существует три способа внедрения зависимостей, и у каждого своя область применения:

| Способ | Описание | Когда использовать |
| --- | --- | --- |
| **Constructor Injection** | Зависимость передается через конструктор и сохраняется в поле | Основной способ. Для обязательных зависимостей, без которых класс не может работать. |
| **Property Injection** | Зависимость устанавливается через публичное свойство после создания объекта | Для опциональных зависимостей (например, логгер). Используется реже. |
| **Method Injection** | Зависимость передается непосредственно в метод | Когда зависимость нужна только для одного метода (например, валидатор для конкретной операции). |

Рассмотрим **Property Injection** на примере. Предположим, у нас есть сервис, который может использовать логгер, но его отсутствие не должно останавливать работу:

csharp

```csharp
public interface ILogger 
{ 
    void Log(string message); 
}

public class OrderService 
{ 
    private readonly IMessageSender _messageSender;

    // Основная зависимость (обязательная) — через конструктор 
    public OrderService(IMessageSender messageSender) 
    { 
        _messageSender = messageSender; 
    }

    // Опциональная зависимость — через свойство 
    public ILogger Logger { get; set; } // ← Property Injection

    public void ProcessOrder(Order order) 
    {
        // Основная логика...
        _messageSender.Send(order.Email, "Заказ принят", "Спасибо!");

        // Логирование только если логгер установлен 
        if (Logger != null) 
        { 
            Logger.Log($"Заказ {order.Id} обработан"); 
        } 
    } 
}
```

В тесте мы можем либо подставить логгер (если нам нужно проверить логирование), либо не подставлять (если логирование не важно для теста):

csharp

```csharp
[Fact] 
public void ProcessOrder_WithLogger_LogsMessage() 
{ 
    // Arrange 
    var mockSender = new Mock<IMessageSender>(); 
    var mockLogger = new Mock<ILogger>();

    var service = new OrderService(mockSender.Object); 
    // ↓ Property Injection в тесте 
    service.Logger = mockLogger.Object;

    // Act 
    service.ProcessOrder(new Order { Id = 123, Email = "test@test.com" });

    // Assert 
    mockLogger.Verify(l => l.Log("Заказ 123 обработан"), Times.Once); 
}
```

**Method Injection** используется, когда зависимость нужна только для одного метода и нет смысла хранить её в полях класса. Рассмотрим пример:

csharp

```csharp
public class ReportGenerator 
{ 
    // Метод принимает зависимость как параметр 
    public string GenerateReport(
        IReportFormatter formatter,  // ↑ Method Injection
        DateTime start, 
        DateTime end
    ) 
    { 
        var data = FetchData(start, end); 
        return formatter.Format(data); 
    } 
}
```

В тесте мы можем передать заглушку напрямую в вызов метода:

csharp

```csharp
[Fact] 
public void GenerateReport_CallsFormatter() 
{ 
    var stubFormatter = new Mock<IReportFormatter>(); 
    stubFormatter
        .Setup(f => f.Format(It.IsAny<Data>()))
        .Returns("Отчет");

    var generator = new ReportGenerator();

    // Method Injection: передаем заглушку как аргумент метода 
    var result = generator.GenerateReport(
        stubFormatter.Object, 
        DateTime.Now.AddDays(-7), 
        DateTime.Now
    );

    Assert.Equal("Отчет", result); 
}
```

Иерархия предпочтений в реальных проектах

1. **Constructor Injection** — используйте всегда, когда зависимость обязательна.
2. **Property Injection** — используйте только для опциональных зависимостей (когда класс может работать и без неё).
3. **Method Injection** — используйте только когда зависимость нужна для одного конкретного метода.

Все обязательные зависимости класса должны быть явно видны в его конструкторе. Это делает код честным и прозрачным. Если класс принимает в конструкторе 5 интерфейсов — это сигнал, что он делает слишком много и нарушает принцип единственной ответственности (SRP). Если класс не принимает ничего, но внутри создает зависимости через `new` — это нетестируемый код.

## §3.8 Принцип инверсии зависимостей (DIP) и его роль в тестировании

В предыдущем параграфе мы рассмотрели Dependency Injection как конкретный паттерн передачи зависимостей. Однако за этим паттерном стоит более глубокий принцип проектирования — **Dependency Inversion Principle (DIP)**, один из пяти принципов SOLID. Понимание этого принципа позволяет осознанно проектировать архитектуру, а не просто механически использовать DI.

**Формулировка DIP:**

Dependency Inversion Principle

1. **Модули верхнего уровня не должны зависеть от модулей нижнего уровня.** Оба должны зависеть от абстракций.
2. **Абстракции не должны зависеть от деталей.** Детали должны зависеть от абстракций.

На практике это означает, что бизнес-логика не должна знать, как именно сохраняются данные, генерируются отчеты или выполняются внешние запросы. Она должна знать только интерфейс для этих действий.

Рассмотрим **нарушение DIP** на примере класса, который генерирует отчет и сохраняет его в базе данных:

csharp

```csharp
public class ReportService 
{ 
    private readonly SqlConnection _connection = new SqlConnection("Server=...");
    // ↑ Зависимость от конкретной реализации

    public void GenerateAndSaveReport(DateTime period) 
    { 
        // 1. Генерация отчета 
        var report = $"Отчет за {period:yyyy-MM}";
        //... логика формирования...

        // 2. Сохранение в базу 
        using var cmd = new SqlCommand("INSERT INTO Reports...", _connection); 
        cmd.ExecuteNonQuery(); 
    } 
}
```

Проблема

`ReportService` жестко привязан к `SqlConnection`. Если мы захотим сохранять отчеты в файл, в облачное хранилище или просто проверить логику генерации в тесте без реальной базы данных — мы не сможем этого сделать без переписывания кода.

**Исправление — соблюдение DIP:**

csharp

```csharp
// Абстракция (интерфейс) 
public interface IReportStorage 
{ 
    void Save(string reportContent); 
}

// Деталь №1 — сохранение в базу данных 
public class DatabaseReportStorage : IReportStorage 
{ 
    private readonly string _connectionString;

    public DatabaseReportStorage(string connectionString) => 
        _connectionString = connectionString;

    public void Save(string reportContent) => 
        // Реальное сохранение в БД
}

// Деталь №2 — сохранение в файл 
public class FileReportStorage : IReportStorage 
{ 
    private readonly string _path;

    public FileReportStorage(string path) => _path = path;

    public void Save(string reportContent) => 
        File.WriteAllText(_path, reportContent); 
}

// Бизнес-логика зависит от абстракции 
public class ReportService 
{ 
    // ↓ Зависимость от интерфейса 
    private readonly IReportStorage _storage;

    public ReportService(IReportStorage storage) => 
        _storage = storage;

    public void GenerateAndSaveReport(DateTime period) 
    { 
        // Генерация отчета 
        var report = $"Отчет за {period:yyyy-MM}";
        //... логика формирования...

        // Сохранение через абстракцию 
        _storage.Save(report); 
    } 
}
```

Теперь `ReportService` не знает, куда именно сохраняется отчет — в базу данных, в файл или в облако. Он знает только, что у объекта есть метод `Save`. Это делает код гибким: мы можем легко добавить новый способ сохранения, просто реализовав интерфейс `IReportStorage`.

Именно благодаря DIP мы можем подставить Stub или Mock в тесты. Если бы `ReportService` зависел от конкретного `SqlConnection`, мы не смогли бы его протестировать без реальной базы данных. Но поскольку он зависит от интерфейса `IReportStorage`, мы в тесте передаем заглушку:

csharp

```csharp
public class StubReportStorage : IReportStorage 
{ 
    public string SavedContent { get; private set; }

    public void Save(string reportContent) 
    { 
        // Сохраняем в память, а не в базу 
        SavedContent = reportContent; 
    } 
}

[Fact] 
public void GenerateAndSaveReport_CreatesCorrectContent() 
{ 
    // Arrange 
    var stubStorage = new StubReportStorage(); 
    var service = new ReportService(stubStorage); 
    var period = new DateTime(2026, 1, 1);

    // Act 
    service.GenerateAndSaveReport(period);

    // Assert 
    Assert.Contains("Отчет за 2026-01", stubStorage.SavedContent); 
    // Проверяем логику генерации, не заботясь о сохранении 
}
```

DI vs DIP: в чем разница?

Понятия DI и DIP часто путают. Проведем четкую границу:

- **DIP (Dependency Inversion Principle)** — это **принцип**. Он отвечает на вопрос «Как правильно спроектировать зависимости между классами?». Ответ: зависеть от интерфейсов, а не от конкретных классов.
- **DI (Dependency Injection)** — это **паттерн**. Он отвечает на вопрос «Как физически передать зависимость в объект?». Ответ: через конструктор, свойство или метод.

**Простая аналогия:**

- **DIP** — это закон дорожного движения: «Ездить по правой стороне».
- **DI** — это руль: инструмент, который позволяет этот закон соблюдать.

DIP говорит «завись от абстракций», а DI дает инструмент — «передавай эти абстракции через конструктор».

## §3.9 Контейнеры IoC: автоматизация управления зависимостями

Мы выяснили, что Dependency Injection — это мощный паттерн, делающий код тестируемым. Однако в реальных проектах, особенно крупных, ручное создание объектов через конструктор превращается в утомительную рутину. Рассмотрим типичный пример создания сервиса с несколькими зависимостями:

csharp

```csharp
var logger = new FileLogger("app.log"); 
var repository = new OrderRepository(connectionString); 
var emailSender = new SmtpEmailSender(smtpConfig); 
var discountCalculator = new DiscountCalculator(); 

var orderService = new OrderService(
    repository, 
    emailSender, 
    discountCalculator, 
    logger
);
```

В проекте с сотней классов такое ручное связывание превращается в кошмар: каждый раз, когда меняется конструктор одного класса (добавляется новая зависимость), приходится править код создания во всех местах. Именно для решения этой проблемы существуют **IoC-контейнеры** (Inversion of Control Containers).

**IoC-контейнер**

— это библиотека, которая берет на себя управление зависимостями. Вы регистрируете в контейнере, какой интерфейс какой реализацией должен быть заменён, а контейнер автоматически создаёт объекты и внедряет зависимости. Вам не нужно вручную писать `new` — контейнер делает это за вас.

**Популярные IoC-контейнеры в .NET:**

| Контейнер | Описание |
| --- | --- |
| **Microsoft.Extensions.DependencyInjection** | Встроенный в .NET Core/ASP.NET Core. Стандарт для современных проектов. |
| **Autofac** | Гибкий и мощный, поддерживает множество расширений (декораторы, модули). |
| **Ninject** | Простой и легковесный, популярен в старых проектах. |
| **Castle Windsor** | Мощный функциональный контейнер, но достаточно тяжеловесный. |

**Пример регистрации и использования** (Microsoft.Extensions.DependencyInjection):

csharp

```csharp
using Microsoft.Extensions.DependencyInjection;

// 1. Создаём коллекцию сервисов (регистрируем зависимости) 
var services = new ServiceCollection();

// Регистрируем: при запросе IReportStorage вернуть FileReportStorage 
services.AddScoped<IReportStorage, FileReportStorage>(); 

// Регистрируем сам сервис (контейнер сам поймёт, что ему нужен IReportStorage) 
services.AddScoped<ReportService>();

// 2. Строим провайдер (контейнер) 
var serviceProvider = services.BuildServiceProvider();

// 3. Получаем готовый объект со всеми зависимостями 
var reportService = serviceProvider.GetRequiredService<ReportService>(); 
// Контейнер сам создал FileReportStorage и 
// передал его в конструктор ReportService
```

### Жизненные циклы (Scope) в контейнерах

При регистрации важно указать, как долго должен жить объект:

| Метод регистрации | Жизненный цикл | Когда использовать |
| --- | --- | --- |
| `AddTransient<T>()` | Создается каждый раз при запросе | Для легковесных, не имеющих состояния объектов |
| `AddScoped<T>()` | Один экземпляр на запрос (в веб-приложении) | Для объектов, которые должны жить в рамках одного HTTP-запроса |
| `AddSingleton<T>()` | Один экземпляр на всё приложение | Для тяжелых объектов (кеши, конфигурация) |

**Пример с разными жизненными циклами:**

csharp

```csharp
// Создаётся каждый раз 
services.AddTransient<IEmailSender, SmtpEmailSender>(); 

// Один на запрос 
services.AddScoped<IOrderRepository, OrderRepository>(); 

// Один на всё приложение 
services.AddSingleton<IConfiguration, AppConfiguration>();
```

Важное различие: Production vs Тесты

Это критически важный момент, который нужно запомнить – это граница ответственности между production-кодом и тестами:

| Среда | Что используем | Почему |
| --- | --- | --- |
| **Production-код** | IoC-контейнер | Автоматизирует создание объектов, упрощает поддержку |
| **Тесты** | Ручной DI (передача Stub/Mock через конструктор) | Нужен полный контроль над зависимостями; контейнеры добавляют лишнюю сложность |

В тестах мы **не используем** контейнер, потому что:

1. Нам нужно подставлять Stub и Mock — контейнер этого не умеет делать (он работает с реальными реализациями).
2. Каждый тест должен иметь изолированное окружение — общий контейнер нарушил бы независимость тестов.
3. Тесты должны быть максимально простыми и понятными — скрытая магия контейнера только запутывает.

**В production-коде:**

csharp

```csharp
// Program.cs 
var services = new ServiceCollection(); 
services.AddScoped<IReportStorage, FileReportStorage>(); 
services.AddScoped<ReportService>(); 

var provider = services.BuildServiceProvider(); 
var service = provider.GetRequiredService<ReportService>();
```

**В тестах:**

csharp

```csharp
// ReportServiceTests.cs 
[Fact] 
public void GenerateReport_ValidData_ReturnsCorrectResult() 
{ 
    // Мы сами создаём зависимости — никакого контейнера 
    var stubStorage = new StubReportStorage(); 
    var service = new ReportService(stubStorage); // ← ручной DI

    var result = service.GenerateReport(...);

    Assert.Equal(expected, result); 
}
```

Итог

**IoC-контейнеры — это инструмент для production-кода**, упрощающий управление зависимостями в больших системах. Однако для тестирования мы **всегда используем ручной DI**, потому что нам нужен полный контроль над каждым объектом. Это разделение ответственности — признак профессионального подхода и понимания архитектуры приложения.
