How to test Ninject ConstructorArguments using MOQ objects?

后端 未结 1 2105
情话喂你
情话喂你 2021-02-06 10:55

I have been doing my first Test Driven Development project recently and have been learning Ninject and MOQ. This is my first attempt at all this. I\'ve found the TDD approach ha

1条回答
  •  执笔经年
    2021-02-06 11:50

    There are a few problems with the way you designed your application. First of all, you are calling the Ninject kernel directly from within your code. This is called the Service Locator pattern and it is considered an anti-pattern. It makes testing your application much harder and you are already experiencing this. You are trying to mock the Ninject container in your unit test, which complicates things tremendously.

    Next, you are injecting primitive types (string, bool) in the constructor of your DirEnum type. I like how MNrydengren states it in the comments:

    take "compile-time" dependencies through constructor parameters and "run-time" dependencies through method parameters

    It's hard for me to guess what that class should do, but since you are injecting these variables that change at run-time into the DirEnum constructor, you end up with a hard to test application.

    There are multiple ways to fix this. Two that come in mind are the use of method injection and the use of a factory. Which one is feasible is up to you.

    Using method injection, your Configurator class will look like this:

    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);
        }
    }
    

    Using a factory, you would need to define a factory that knows how to create new IDirEnum types:

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

    Your Configuration class can now depend on the IDirEnumFactory interface:

    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);
        }
    }
    

    See how in both examples the dependencies get injected into the Configurator class. This is called the Dependency Injection pattern, opposed to the Service Locator pattern, where the Configurator asks for its dependencies by calling into the Ninject kernel.

    Now, since your Configurator is completely free from any IoC container what so ever, you can now easily test this class, by injecting a mocked version of the dependency it expects.

    What is left is to configure the Ninject container in the top of your application (in DI terminology: the composition root). With the method injection example, your container configuration would stay the same, with the factory example, you will need to replace the Bind().To() line with something as follows:

    public static void Bootstrap()
    {
        kernel.Bind().To();
    }
    

    Of course, you will need to create the DirEnumFactory:

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

    WARNING: Do note that factory abstractions are in most cases not the best design, as explained here.

    The last thing you need to do is to create a new Configurator instance. You can simply do this as follows:

    public static Configurator CreateConfigurator()
    {
        return kernel.Get();
    }
    
    public static void Main(string[] args)
    {
        Bootstrap():
        var configurator = CreateConfigurator();
    
        configurator.ConfigureServices(args);
    }
    

    Here we call the kernel. Although calling the container directly should be prevented, there will always at least be one place in your application where you call the container, simply because it must wire everything up. However, we try to minimize the number of times the container is called directly, because it improves -among other things- the testability of our code.

    See how I didn't really answer your question, but showed a way to work around the problem very effectively.

    You might still want to test your DI configuration. That's very valid IMO. I do this in my applications. But for this, you often don't need the DI container, or even if your do, this doesn't mean that all your tests should have a dependency on the container. This relationship should only exist for the tests that test the DI configuration itself. Here is a test:

    [TestMethod]
    public void DependencyConfiguration_IsConfiguredCorrectly()
    {
        // Arrange
        Program.Bootstrap();
    
        // Act
        var configurator = Program.CreateConfigurator();
    
        // Assert
        Assert.IsNotNull(configurator);
    }
    

    This test indirectly depends on Ninject and it will fail when Ninject is not able to construct a new Configurator instance. When you keep your constructors clean from any logic and only use it for storing the taken dependencies in private fields, you can run this, without the risk of calling out to a database, web service or what so ever.

    I hope this helps.

    0 讨论(0)
提交回复
热议问题