如何使用MOQ对象测试Ninject ConstructorArguments?

JBo*_*wen 6 tdd dependency-injection ninject moq

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

我已将其分解为模块并在整个商店中都有接口,但我仍然发现在从Ninject内核获取服务的实现时我必须使用大量的构造函数参数.例如;

在我的Ninject模块中;

Bind<IDirEnum>().To<DirEnum>()
Run Code Online (Sandbox Code Playgroud)

我的DirEnum课程;

public class DirEnum : IDirEnum
{
    public DirEnum(string filePath, string fileFilter, 
        bool includeSubDirs)
    {
        ....
Run Code Online (Sandbox Code Playgroud)

在我的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)
        );
Run Code Online (Sandbox Code Playgroud)

filePath,fileFilter和includeSubDirs是程序的命令行选项.到现在为止还挺好.然而,作为一个尽职尽责的人,我有一个覆盖这段代码的测试.我想使用MOQ对象.我为我的测试创建了一个Ninject模块;

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

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

[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???
Run Code Online (Sandbox Code Playgroud)

所以上面显示了我的问题.如何测试传递给模拟对象的ConstructorArguments的参数?我的猜测是Ninject在这种情况下分配ConstuctorArguments,因为Bind不需要它们?我可以使用MOQ对象测试它,还是需要手动编写实现DirEnum的模拟对象并接受并"记录"构造函数参数?

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

谢谢你的期待.要温柔,这是我的第一次;-)

吉姆

Ste*_*ven 15

您设计应用程序的方式存在一些问题.首先,您直接在代码中调用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

interface IDirEnumFactory
{
    IDirEnum CreateDirEnum(string filePath, string fileFilter, 
        bool includeSubDirs);
}
Run Code Online (Sandbox Code Playgroud)

您的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

现在,既然你Configurator完全没有任何IoC容器,你现在可以通过注入它所期望的依赖的模拟版本来轻松地测试这个类.

剩下的就是在应用程序的顶部配置Ninject容器(在DI术语中:组合根).使用方法注入示例,您的容器配置将保持不变,使用工厂示例,您将需要使用以下内容替换该Bind<IDirEnum>().To<DirEnum>()行:

public static void Bootstrap()
{
    kernel.Bind<IDirEnumFactory>().To<DirEnumFactory>();
}
Run Code Online (Sandbox Code Playgroud)

当然,您需要创建DirEnumFactory:

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

警告:请注意,工厂抽象是在大多数情况下不是最好的设计,为解释在这里.

您需要做的最后一件事是创建一个新Configurator实例.您可以按如下方式执行此操作:

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

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

    configurator.ConfigureServices(args);
}
Run Code Online (Sandbox Code Playgroud)

在这里我们称之为内核.虽然应该防止直接调用容器,但是在应用程序中始终至少有一个地方可以调用容器,因为它必须连接所有内容.但是,我们尝试最小化容器被直接调用的次数,因为它改进了其他东西 - 代码的可测试性.

看看我没有真正回答你的问题,但展示了一种非常有效地解决问题的方法.

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

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

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

    // Assert
    Assert.IsNotNull(configurator);
}
Run Code Online (Sandbox Code Playgroud)

此测试间接依赖于Ninject,当Ninject无法构造新Configurator实例时,它将失败.当你保持你的构造函数不受任何逻辑的影响并且仅用于在私有字段中存储所采用的依赖项时,你可以运行它,而不会有调用数据库,Web服务或者其他任何东西的风险.

我希望这有帮助.

  • 很好的答案:) (2认同)