Как проверить Ninject ConstructorArguments, используя объекты MOQ? - PullRequest
6 голосов
/ 07 июня 2011

Недавно я делал свой первый проект по разработке через тестирование и изучал 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)
    {
        ....

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

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 и принимает и «записывает» аргументы конструктора?

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

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

Джим

1 Ответ

15 голосов
/ 08 июня 2011

Есть несколько проблем с тем, как вы разработали свое приложение.Прежде всего, вы вызываете ядро ​​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.Когда вы сохраняете свои конструкторы чистыми от какой-либо логики и используете ее только для хранения взятых зависимостей в частных полях, вы можете запускать это без риска вызова базы данных, веб-службы или чего-либо подобного.

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

...