如何使用MOQ对象测试Ninject ConstructorArguments? [英] How to test Ninject ConstructorArguments using MOQ objects?

查看:57
本文介绍了如何使用MOQ对象测试Ninject ConstructorArguments?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我最近一直在做我的第一个测试驱动开发"项目,并且一直在学习Ninject和MOQ.这是我所有的第一次尝试.我发现TDD方法令人发指,Ninject和MOQ很棒.我正在从事的项目并不是特别适合Ninject,因为它是一个高度可配置的C#程序,旨在测试Web服务接口的使用.

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 has been thought provoking, and Ninject and MOQ have been great. The project I am working on has not particularly been the best fit for Ninject as it is a highly configurable C# program that is designed to test the use of a web service interface.

我已经将其分解为模块,并且在整个工厂中都有接口,但是我仍然发现从Ninject内核获取服务的实现时,我不得不使用许多构造函数参数.例如;

I have broken it up into modules and have interfaces all over the shop, but I am still finding that I am having to use lots of constructor arguments when getting an implementation of a service from the Ninject kernel. For example;

在我的Ninject模块中;

In my Ninject module;

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

我的DirEnum类;

My DirEnum class;

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

在我的Configurator类(这是主要入口点)中,它将所有服务挂钩在一起;

In my Configurator class (this is the main entry point) that hooks all the services together;

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是程序的命令行选项.到目前为止,一切都很好.但是,作为一个有责任心的人,我对这部分代码进行了测试.我想使用最小起订量对象.我已经为测试创建了一个Ninject模块;

filePath, fileFilter and includeSubDirs are command line options to the program. So far so good. However, being a conscientious kind of guy, I have a test covering this bit of code. I'd like to use a MOQ object. I have created a Ninject module for my tests;

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

在我的测试中,我像这样使用它;

And in my test I use it like this;

[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并接受并记录"构造函数参数的模拟对象?

So the above shows my problem. How can I test what arguments were passed to the ConstructorArguments of the mock object? My guess is that Ninject is dispensing of the ConstuctorArguments in this case as the Bind does not require them? Can I test this with a MOQ object or do I need to hand code a mock object that implements DirEnum and accepts and 'records' the constructor arguments?

n.b.该代码是示例"代码,即我没有逐字复制代码,但我想我已经表达了足够的希望来传达这些问题?如果您需要更多背景信息,请询问!

n.b. this code is 'example' code, i.e. I have not reproduced my code verbatim, but I think I have expressed enough to hopefully convey the issues? If you need more context, please ask!

感谢您的光临.温柔,这是我第一次;-)

Thanks for looking. Be gentle, this is my first time ;-)

吉姆

推荐答案

设计应用程序的方式存在一些问题.首先,您正在直接从代码内调用Ninject内核.这称为服务定位器模式

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.

接下来,您要在DirEnum类型的构造函数中注入基本类型(stringbool).我喜欢MNrydengren在评论中所说的:

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

我很难猜测该类应该做什么,但是由于您将这些在运行时更改的变量注入到DirEnum构造函数中,因此最终导致难以测试的应用程序.

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.

使用方法注入,您的Configurator类将如下所示:

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

使用工厂,您需要定义一个知道如何创建新的IDirEnum类型的工厂:

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

您的Configuration类现在可以依赖于IDirEnumFactory界面:

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

查看两个示例中的依赖项如何注入到Configurator类中.这称为依赖注入模式,与服务定位器模式相对,在Configurator通过调用Ninject内核来询问其依赖性.

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.

现在,由于您的Configurator完全不受任何IoC容器的限制,因此您现在可以通过注入其期望的依赖关系的模拟版本来轻松测试此类.

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.

剩下的是在应用程序顶部配置Ninject容器(使用DI术语:

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<IDirEnum>().To<DirEnum>() line with something as follows:

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

警告:请注意,在大多数情况下,工厂抽象不是最佳设计,如

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

您需要做的最后一件事是创建一个新的Configurator实例.您可以简单地执行以下操作:

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

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.

您可能仍想测试DI配置.那是非常有效的海事组织.我在我的应用程序中执行此操作.但是为此,您通常不需要DI容器,或者即使您需要DI容器,也并不意味着您的所有测试都应该依赖于该容器.此关系仅应存在于测试DI配置本身的测试中.这是一个测试:

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

此测试间接依赖于Ninject,当Ninject无法构造新的Configurator实例时,它将失败.当您使构造函数远离任何逻辑并仅将其用于将获取的依赖项存储在私有字段中时,就可以运行此函数,而不必担心会调用数据库,Web服务或其他任何东西.

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.

我希望这会有所帮助.

这篇关于如何使用MOQ对象测试Ninject ConstructorArguments?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆