Как протестировать Ninject ConstructorArguments с помощью объектов MOQ?

Недавно я выполнял свой первый проект по разработке через тестирование и изучал Ninject и MOQ. Это моя первая попытка. Я обнаружил, что подход TDD заставляет задуматься, а Ninject и MOQ оказались великолепными. Проект, над которым я работаю, не особенно подходит для Ninject, поскольку это программа на C # с широкими возможностями настройки, предназначенная для тестирования использования интерфейса веб-службы.

Я разбил его на модули и имею интерфейсы по всему магазину, но я все еще обнаруживаю, что мне приходится использовать множество аргументов конструктора при получении реализации службы из ядра Ninject. Например;

В моем модуле Ninject;

Bind<IDirEnum>().To<DirEnum>()

Мой класс DirEnum;

public class DirEnum : IDirEnum
{
    public DirEnum(string filePath, string fileFilter, 
        bool includeSubDirs)
    {
        ....

В моем классе конфигуратора (это основная точка входа), который объединяет все службы;

class Configurator
{

    public ConfigureServices(string[] args)
    {
        ArgParser argParser = new ArgParser(args);
        IDirEnum dirEnum = kernel.Get<IDirEnum>(
            new ConstructorArgument("filePath", argParser.filePath),
            new ConstructorArgument("fileFilter", argParser.fileFilter),
            new ConstructorArgument("includeSubDirs", argParser.subDirs)
        );

filePath, fileFilter и includeSubDirs - это параметры командной строки для программы. Все идет нормально. Однако, как человек сознательный, у меня есть тест, охватывающий этот фрагмент кода. Я хочу использовать объект MOQ. Я создал модуль Ninject для своих тестов;

public class TestNinjectModule : NinjectModule
{
    internal IDirEnum mockDirEnum {set;get};
    Bind<IDirEnum>().ToConstant(mockDirEnum);
}

И в своем тесте я использую это так:

[TestMethod]
public void Test()
{
    // Arrange
    TestNinjectModule testmodule = new TestNinjectModule();
    Mock<IDirEnum> mockDirEnum = new Mock<IDirEnum>();
    testModule.mockDirEnum = mockDirEnum;
    // Act
    Configurator configurator = new Configurator();
    configurator.ConfigureServices();
    // Assert

    here lies my problem! How do I test what values were passed to the
    constructor arguments???

Итак, приведенное выше показывает мою проблему. Как я могу проверить, какие аргументы были переданы в ConstructorArguments фиктивного объекта? Я предполагаю, что в этом случае Ninject распределяет ConstuctorArguments, поскольку Bind их не требует? Могу ли я проверить это с помощью объекта MOQ или мне нужно передать код фиктивного объекта, который реализует DirEnum и принимает и «записывает» аргументы конструктора?

n.b. этот код является «примерным» кодом, т.е. я не воспроизвел свой код дословно, но я думаю, что я выразил достаточно, чтобы, надеюсь, передать проблемы? Если вам нужно больше контекста, спросите!

Спасибо, что искали. Будь нежным, это мой первый раз ;-)

Джим


person JBowen    schedule 07.06.2011    source источник
comment
Что делает DirEnum? Мое общее практическое правило - принимать зависимости времени компиляции через параметры конструктора, а зависимости времени выполнения - через параметры метода. Поскольку 'filePath', 'fileFilter' и 'includeSubDirs' являются аргументами командной строки, я считаю, что они зависят от времени выполнения и, следовательно, должны передаваться как параметры метода методу, который в них нуждается.   -  person mrydengren    schedule 07.06.2011
comment
@mrydengren: Мне нравится, как это звучит: время компиляции для конструктора и время выполнения для параметров метода ..   -  person Steven    schedule 07.06.2011


Ответы (1)


Есть несколько проблем с тем, как вы разрабатываете свое приложение. Прежде всего, вы вызываете ядро ​​Ninject прямо из своего кода. Это называется шаблоном локатора службы и считается анти-шаблоном. Это значительно усложняет тестирование вашего приложения, и вы уже сталкиваетесь с этим. Вы пытаетесь имитировать контейнер Ninject в своем модульном тесте, что чрезвычайно усложняет ситуацию.

Затем вы вводите примитивные типы (string, bool) в конструктор вашего DirEnum типа. Мне нравится, как MNrydengren заявляет об этом в комментариях:

принимать зависимости "времени компиляции" через параметры конструктора и зависимости "времени выполнения" через параметры метода

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

Есть несколько способов исправить это. Два, которые приходят в голову, - это использование внедрения метода и использование фабрики. Какой из них осуществим - решать вам.

При использовании внедрения метода ваш класс Configurator будет выглядеть так:

class Configurator
{
    private readonly IDirEnum dirEnum;

    // Injecting IDirEnum through the constructor
    public Configurator(IDirEnum dirEnum)
    {
        this.dirEnum = dirEnum;
    }

    public ConfigureServices(string[] args)
    {
        var parser = new ArgParser(args);

        // Inject the arguments into a method
        this.dirEnum.SomeOperation(
            argParser.filePath
            argParser.fileFilter
            argParser.subDirs);
    }
}

Используя фабрику, вам нужно будет определить фабрику, которая знает, как создавать новые IDirEnum типы:

interface IDirEnumFactory
{
    IDirEnum CreateDirEnum(string filePath, string fileFilter, 
        bool includeSubDirs);
}

Ваш Configuration класс теперь может зависеть от IDirEnumFactory интерфейса:

class Configurator
{
    private readonly IDirEnumFactory dirFactory;

    // Injecting the factory through the constructor
    public Configurator(IDirEnumFactory dirFactory)
    {
        this.dirFactory = dirFactory;
    }

    public ConfigureServices(string[] args)
    {
        var parser = new ArgParser(args);

        // Creating a new IDirEnum using the factory
        var dirEnum = this.dirFactory.CreateDirEnum(
            parser.filePath
            parser.fileFilter
            parser.subDirs);
    }
}

Посмотрите, как в обоих примерах зависимости вводятся в класс Configurator. Это называется шаблоном внедрения зависимостей, в отличие от шаблона Service Locator, где Configurator запрашивает его зависимости, вызывая ядро ​​Ninject.

Теперь, поскольку ваш Configurator полностью свободен от какого-либо контейнера IoC, вы можете легко протестировать этот класс, внедрив имитируемую версию зависимости, которую он ожидает.

Осталось настроить контейнер Ninject в верхней части вашего приложения (в терминологии DI: корень композиции). В примере внедрения метода конфигурация вашего контейнера останется прежней, в примере с фабрикой вам нужно будет заменить строку Bind<IDirEnum>().To<DirEnum>() на что-то вроде следующего:

public static void Bootstrap()
{
    kernel.Bind<IDirEnumFactory>().To<DirEnumFactory>();
}

Конечно, вам нужно будет создать DirEnumFactory:

class DirEnumFactory : IDirEnumFactory
{
    IDirEnum CreateDirEnum(string filePath, string fileFilter, 
        bool includeSubDirs)
    {
        return new DirEnum(filePath, fileFilter, includeSubDirs);
    }        
}

ВНИМАНИЕ: обратите внимание, что фабричные абстракции в большинстве случаев не являются лучшим дизайном, как объяснялось в здесь.

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

public static Configurator CreateConfigurator()
{
    return kernel.Get<Configurator>();
}

public static void Main(string[] args)
{
    Bootstrap():
    var configurator = CreateConfigurator();

    configurator.ConfigureServices(args);
}

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

Посмотрите, как я не ответил на ваш вопрос, но показал способ очень эффективного решения проблемы.

Возможно, вы все равно захотите протестировать свою конфигурацию DI. Это очень актуально, ИМО. Я делаю это в своих приложениях. Но для этого вам часто не нужен контейнер DI, или даже если он у вас есть, это не означает, что все ваши тесты должны иметь зависимость от контейнера. Эта связь должна существовать только для тестов, которые проверяют саму конфигурацию DI. Вот тест:

[TestMethod]
public void DependencyConfiguration_IsConfiguredCorrectly()
{
    // Arrange
    Program.Bootstrap();

    // Act
    var configurator = Program.CreateConfigurator();

    // Assert
    Assert.IsNotNull(configurator);
}

Этот тест косвенно зависит от Ninject и завершится ошибкой, если Ninject не сможет создать новый Configurator экземпляр. Когда вы очищаете свои конструкторы от какой-либо логики и используете их только для хранения взятых зависимостей в частных полях, вы можете запускать это без риска обращения к базе данных, веб-сервису или чему-то еще.

Надеюсь, это поможет.

person Steven    schedule 07.06.2011
comment
Спасибо, @ Стивен. Я сейчас перефакторингу, и у меня есть несколько заводов, которые вводятся. Таким образом, я настраиваю все зависимости статических сервисов и сервисы, которые мне могут потребоваться, а могут и не потребоваться в зависимости от конфигурации времени выполнения, которую я получаю от фабрик. Это значительно упростило модульное тестирование, и теперь мне не нужно передавать ядро ​​Ninject каким-либо тестам, кроме корневого теста, где я проверяю логику метода, который получает IConfigurator из ядра. Спасибо, что нашли время так быстро ответить! Большой респект :-) - person JBowen; 09.06.2011
comment
Модульный тест не должен зависеть от контейнера DI. В таком случае вам, вероятно, следует использовать mocks для имитации всех зависимостей? - person Dariusz; 10.06.2011
comment
@Dario: В самом деле: вам не нужны ссылки на какой-либо контейнер IoC в ваших тестах, что бы то ни было; ожидайте, конечно, тех тестов, которые проверяют саму конфигурацию DI. Чтобы протестировать класс изолированно (это и есть модульное тестирование), вам необходимо снабдить его поддельными зависимостями (или передать null, если зависимость не используется в этом конкретном тесте). Являются ли они фиктивными объектами или нет, зависит от того, что вы хотите протестировать. Благодаря чистому дизайну приложения и чистым модульным тестам вам вряд ли понадобится фреймворк для фиксации. Кроме того, макетные фреймворки засоряют модульные тесты техническими деталями. - person Steven; 10.06.2011