Сегодня использование контейнеров зависимостей является стандартной практикой. Ваши классы принимают в конструкторе экземпляры других классов, те в свою очередь зависят от третьих классов и т. д. И сборкой всего этого управляет контейнер зависимостей. Эта система имеет свою цену. Например, во время тестирования вам приходится как‑то создавать экземпляры всех принимаемых конструктором параметров, чтобы протестировать ваш класс. Для этого можно использовать что‑то типа Moq. Но в таком случае встаёт проблема изменений класса. Если вы хотите добавить или удалить какой‑нибудь параметр из конструктора, вам приходится менять и тесты, даже если на них данный параметр никак не влияет. Есть и ещё одна задача, которую хотелось бы решать при тестировании. Предположим, что вы хотите проверить работу не одного изолированного класса, а совместную работу классов некоторой части вашей системы. Ваш контейнер зависимостей строит для вас целое дерево из экземпляров различных классов. И вам хочется протестировать его работу целиком. Давайте посмотрим, как это можно сделать, какие трудности встречаются на этом пути, и как их можно обойти. Пусть у нас есть некий класс, который мы хотим тестировать: Обычно тесты для него пишутся в следующей манере: Но вот пришло время, когда мне потребовалось добавить в класс System логирование: Теперь мои тесты даже не компилируются. Мне нужно пойти во все места, где я создаю экземпляры моего класса System и исправить там код: Несомненно, чтобы сократить работу, я могу вынести создание экземпляра класса в отдельный метод. Тогда мне не придётся проделывать эти изменения в каждом тесте: Но и у этого подхода есть недостатки. Мне всё же пришлось создать заглушку для ILogger, хотя в тестах она мне совершенно не нужна. Я использую её только для передачи в конструктор моего класса. К счастью, существует AutoMocker. Вы просто создаёте экземпляр вашего класса с помощью вызова CreateInstance: Данный метод способен создавать экземпляры любых классов, даже sealed. Он работает подобно контейнеру зависимостей, анализируя конструктор и создавая заглушки для его параметров. В любой момент вы можете получить нужную вам заглушку, чтобы установить её поведение или проверить совершённые на ней вызовы: Кроме того, если вы хотите подсунуть вашему классу не заглушку Moq, а вашу собственную реализацию, то до вызова CreateInstance можно сделать и это: Красота! Теперь можно смело менять сигнатуру конструктора не опасаясь, что придётся править тесты в тысяче мест. Впрочем, тот факт, что тесты продолжают компилироваться, не означает, что они будут продолжать проходить после внесения изменений в класс. С другой стороны, как неоднократно говорилось, хорошие тесты должны проверять контракт класса, а не его внутреннюю реализацию. Если контракт не поменялся, то и тесты должны продолжать проходить. Если же поменялся контракт, то и изменения тестов не избежать. Правда до того, как мы начали использовать AutoMocker мы сразу видели, какие тесты затронуло наше изменение конструктора и могли перезапустить только их. Теперь, возможно, придётся перезапустить все тесты, чтобы проверить ваши изменения, если у вас нет соглашения о том, куда помещать все тесты, касающиеся одного класса. Но тут уж каждый решает для себя, какой подход выбирать. А мы движемся дальше. Один мой коллега предложил пойти дальше. На самом деле в нашем приложении мы всё равно строим контейнер зависимостей, в котором регистрируем все наши классы и интерфейсы. Так почему бы не заставить наши тесты брать экземпляры классов для тестирования из этого контейнера. В таком случае мы будем тестировать именно то дерево объектов, которое используется в реальной работе. Это было бы полезно для интеграционных тестов. Например, наш код регистрации зависимостей имеет вид: Мы выносим эту регистрацию в отдельный метод: и используем его для регистрации наших сервисов: Но теперь мы используем этот код и в тестах: И даже если ваш класс не зарегистрирован в контейнере зависимостей, а вы просто хотите взять оттуда параметры для его конструктора, это можно сделать так: Естественно, может потребоваться внести некоторые изменения в зарегистрированные сервисы. Например, вы можете захотеть подменить строки подключения к базам данных, если вы не используете для их получения IConfiguration: В случае же использования IConfiguration, вы можете просто создать свою конфигурацию, например, с помощью хранилища в оперативной памяти: Но даже в этом случае отказываться от заглушек Moq полностью не хочется. Дело в том, что даже в случае интеграционных тестов существуют внешние зависимости, которые вы не контролируете. Если вашу базу данных вы ещё можете создавать и управлять ей, то существуют внешние сервисы, к которым ваш код может обращаться, например, через HTTP-запросы. Такие внешние запросы всё ещё требуется имитировать с помощью заглушек. Для их поддержки придётся написать немного кода. Я вынес всю логику, касающуюся получения экземпляров сервисов, а так же для управления заглушками, в отдельный класс: Давайте рассмотрим этот класс более подробно. Данный класс реализует интерфейс IServiceProvider, поэтому вы можете использовать все возможности этого интерфейса для получения экземпляров сервисов. Кроме того, метод CreateInstance позволяет вам создавать экземпляры классов, которые не зарегистрированы в контейнере, но чьи параметры конструктора могут быть получены из контейнера. Данный класс перед получением любого сервиса создаёт область видимости (поле _scope). Это позволяет получать даже сервисы, зарегистрированные для одной области видимости (например, с помощью метода AddScope). Область видимости уничтожается в методе Dispose. Именно для этого класс и реализует интерфейс IDisposable. А теперь про получение заглушек (метод GetMock). Здесь реализуется следующая идея. Заглушку для любого сервиса можно создать до тех пор, пока вы не запросили у контейнера первый сервис. После этого создавать новые заглушки нельзя. Причина в том, что контейнер создаст сервис, используя определённые экземпляры классов зависимостей. Т. е. объект сервиса может иметь ссылки на экземпляры этих классов. И заменить эти экземпляры на заглушки уже никак не получится. Поэтому заглушки, созданные после получения сервиса, фактически являются бесполезными. Поэтому мы и не позволяем создавать их. Все уже созданные нами заглушки хранятся в словаре _registeredMocks. Поле _configurationIsFinished хранит информацию о том, запросили ли мы хоть один сервис или ещё нет. Обратите внимание, что при создании заглушки мы удаляем из контейнера все записи для данного типа и добавляем на их место только одну эту заглушку. Если вам потребуется тестировать код, который принимает не один объект данного типа, а целую коллекцию, такой подход может оказаться недостаточным. В этом случае функциональность нашего класса придётся расширить так, как вам это требуется. До сих пор мы использовали наш контейнер зависимостей для того, чтобы тестировать всё приложение целиком. Но есть и ещё один вариант тестирования. У нас весь solution приложения разбит на несколько секций в соответствие с доменными областями. Каждая такая секция может содержать несколько проектов (сборок) - для классов доменной области, для классов инфраструктуры, и т. д. Например: Users.Domain Users.Repository Users.Api или Orders.Domain Orders.Repository Orders.Api И каждый проект предоставляет метод расширения для IServiceCollection, регистрирующий описанные в нём классы: В итоге главный проект приложения только использует у себя эти методы расширения. Предположим, что мы хотим создать тесты уровня проекта. Т. е. мы хотим протестировать взаимодействие только тех классов, которые описаны в данном проекте. Вроде бы всё просто. Создаём экземпляр ServiceCollection, выполняем на нём наш метод расширения, и вот мы в той же ситуации, что и при тестировании всего приложения. Но у этой ситуации есть серьёзное отличие. Когда мы тестировали всё приложение, в нашем экземпляре ServiceCollection были зарегистрированы абсолютно все классы, требуемые для получения любого экземпляра сервиса. В случае же отдельного проекта это не так. Его метод расширения, регистрирующий сервисы, регистрирует только то, что находится в нём самом. Но описанные в проекте классы вполне могут ссылаться на интерфейсы, которые не реализованы в данном проекте, а реализуются в других местах. Например, наш класс System зависит от интерфейсов IService1 и IService2. Оба этих интерфейса объявлены в том же проекте, что и класс System. Но интерфейс IService1 имеет в этом проекте реализацию Service1, а интерфейс IService2 не имеет такой реализации. Ожидается, что он будет реализован в другом проекте и наше приложение будет брать его оттуда. Как же нам тестировать класс System только с классами его проекта? Идея заключается в том, чтобы наш контейнер зависимостей использовал заглушки в случае, если требуемый интерфейс в нём не зарегистрирован. Для этого нам потребуется такой контейнер зависимостей, который мог бы обрабатывать ситуацию отсутствия зарегистрированной зависимости. Я использовал DryIoc. Давайте посмотрим, как с его помощью построить требуемую нам функциональность: Класс Configurator похож на представленный ранее класс SimpleConfigurator, но имеет ряд важных отличий. Во-первых, мы не используем контейнер зависимостей от Microsoft, вместо него используется DryIoc. Для него настраивается поведение на случай, если требуется некоторая незарегистрированная в нём зависимость: В таком случае мы создаём Moq-заглушку и сохраняем ссылку на неё, чтобы позднее её можно было конфигурировать или верифицировать. Теперь мы можем тестировать нашу систему вместе с классами из её проекта: Конечно же, можно не ограничиваться одним проектом. Таким образом можно тестировать, например, классы из группу проектов, относящихся к одной доменной области. В этой статье мы обсудили, как можно использовать имеющуюся инфраструктуру контейнера зависимостей для наших тестов. Несомненно, здесь присутствует множество возможностей для улучшений. Но, надеюсь, я предоставил вам базу, которая будет вам полезной. P.S. Исходный код примеров вы можете найти на GitHub.  Программист