unit-testing
Внедрение зависимости
Поиск…
замечания
Один из подходов, который можно использовать для написания программного обеспечения, - это создание зависимостей по мере необходимости. Это довольно интуитивный способ написать программу, и это способ, которым большинство людей будут учиться, отчасти потому, что легко следовать. Одна из проблем, связанных с этим подходом, заключается в том, что ее трудно проверить. Рассмотрим метод, который выполняет некоторую обработку на основе текущей даты. Метод может содержать некоторый код, например:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
Код имеет прямую зависимость от текущей даты. Этот метод трудно проверить, потому что текущую дату нельзя легко манипулировать. Один из подходов к обеспечению большей проверки кода - это удалить прямую ссылку на текущую дату и вместо этого предоставить (или ввести) текущую дату методу, который выполняет обработку. Эта инъекция зависимостей может значительно облегчить проверку аспектов кода с помощью тестовых удвоений, чтобы упростить шаг настройки модульного теста.
Системы МОК
Еще один аспект, который следует рассмотреть, - время жизни зависимостей; в случае, когда сам класс создает свои собственные зависимости (также известные как инварианты), тогда он ответственен за их удаление. Инъекция зависимостей инвертирует это (и поэтому мы часто ссылаемся на библиотеку инъекций как на систему «Инверсия контроля») и означает, что вместо класса, ответственного за создание, управление и очистку его зависимостей, внешний агент (в этом случай, система IoC) делает это вместо этого.
Это делает гораздо проще иметь зависимости, которые совместно используются экземплярами одного и того же класса; например, рассмотрим службу, которая извлекает данные из конечной точки HTTP для потребляемого класса. Поскольку эта служба не имеет статуса (т. Е. Она не имеет внутреннего состояния), поэтому нам действительно нужен только один экземпляр этой службы в нашем приложении. Хотя это возможно (например, используя статический класс), чтобы сделать это вручную, гораздо проще создать класс и сообщить системе IoC, что он должен быть создан как Singleton , в котором существует только один экземпляр класса.
Другим примером могут быть контексты базы данных, используемые в веб-приложении, в результате чего требуется новый Контекст для каждого запроса (или потока), а не для экземпляра контроллера; это позволяет контексту вводить в каждый слой, выполняемый этим потоком, без необходимости вручную переносить его.
Это освобождает классы-потребители от необходимости управлять зависимостями.
Инъекция конструктора
Инъекция конструктора - самый безопасный способ инъекции зависимостей, от которых зависит весь класс. Такие зависимости часто называют инвариантами , поскольку экземпляр класса не может быть создан без их доставки. Требуя, чтобы зависимость была введена при построении, гарантируется, что объект не может быть создан в несогласованном состоянии.
Рассмотрим класс, который необходимо записать в файл журнала в условиях ошибки. Он имеет зависимость от ILogger
, который можно вводить и использовать, когда это необходимо.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
Иногда при написании тестов вы можете заметить, что конструктор требует больше зависимостей, чем это действительно необходимо для проверяемого случая. Чем больше таких тестов у вас, тем больше вероятность того, что ваш класс нарушит принцип единой ответственности (SRP). Вот почему это не очень хорошая практика, чтобы определить поведение по умолчанию для всех маков вложенных зависимостей на этапе инициализации класса тестирования, поскольку оно может маскировать потенциальный предупреждающий сигнал.
Unittest для этого будет выглядеть следующим образом:
[Test]
public void RecordProcessor_DependencyInjectionExample()
{
ILogger logger = new FakeLoggerImpl(); //or create a mock by a mocking Framework
var sut = new RecordProcessor(logger); //initialize with fake impl in testcode
Assert.IsTrue(logger.HasCalledExpectedMethod());
}
Инъекция недвижимости
Включение свойств позволяет обновлять зависимости классов после их создания. Это может быть полезно, если вы хотите упростить создание объекта, но все же разрешите переопределения зависимостей вашими испытаниями с двойным тестированием.
Рассмотрим класс, который необходимо записать в файл журнала в состоянии ошибки. Класс знает , как построить по умолчанию Logger
, но позволяет ему быть перекрыт через инъекцию собственности. Однако стоит отметить, что при использовании вложения свойств таким образом вы тесно ILogger
этот класс с точной реализацией ILogger
который является ConcreteLogger
в данном примере. Возможным обходным решением может быть фабрика, которая возвращает необходимую реализацию ILogger.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
В большинстве случаев Constructor Injection предпочтительнее Injection Property, потому что она обеспечивает лучшие гарантии состояния объекта сразу после его построения.
Метод инъекции
Метод инъекции представляет собой мелкозернистый способ инъекции зависимостей в обработку. Рассмотрим метод, который выполняет некоторую обработку на основе текущей даты. Текущую дату трудно изменить из теста, поэтому гораздо проще передать дату в метод, который вы хотите проверить.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Контейнеры / DI Frameworks
В то время как извлечение зависимостей из вашего кода, чтобы они могли быть введены, делает ваш код более легким для тестирования, это еще больше увеличивает проблему иерархии и может также привести к сложным конструкциям. Для преодоления этой проблемы были написаны различные схемы инъекций зависимостей / инверсия контрольных контейнеров. Они позволяют регистрировать типы. Эти регистрации затем используются для разрешения зависимостей, когда контейнеру предлагается создать объект.
Рассмотрим эти классы:
public interface ILogger {
void Log(string message);
}
public class ConcreteLogger : ILogger
{
public ConcreteLogger()
{
// ...
}
public void Log(string message)
{
// ...
}
}
public class SimpleClass
{
public SimpleClass()
{
// ...
}
}
public class SomeProcessor
{
public SomeProcessor(ILogger logger, SimpleClass simpleClass)
{
// ...
}
}
Чтобы построить SomeProcessor
, необходимы как экземпляр ILogger
и SimpleClass
. Контейнер, подобный Unity, помогает автоматизировать этот процесс.
Сначала необходимо сконструировать контейнер, а затем регистрировать его. Обычно это делается только один раз в приложении. Область системы, в которой это происходит, обычно известна как Корень Композиции
// Register the container
var container = new UnityContainer();
// Register a type mapping. This allows a `SimpleClass` instance
// to be constructed whenever it is required.
container.RegisterType<SimpleClass, SimpleClass>();
// Register an instance. This will use this instance of `ConcreteLogger`
// Whenever an `ILogger` is required.
container.RegisterInstance<ILogger>(new ConcreteLogger());
После того, как контейнер сконфигурирован, его можно использовать для создания объектов, при необходимости автоматически определяющих зависимости:
var processor = container.Resolve<SomeProcessor>();