zoukankan      html  css  js  c++  java
  • 梦想成现实:用xUnit.net在单元测试中实现构造函数依赖注入

    英文关键词:Constructor Dependency Injection and Unit Testing(为了方便英文搜索)

    自从博客园开发团队将开发架构迁移至DDD(领域驱动开发),就开始正式使用“构造函数依赖注入”,由CNBlogs.Infrastructure.CrossCutting.IoC(IoC容器抽象层,目前默认容器用的是Unity)负责。

    通过构造函数进行依赖注入,避免了在代码中针对所依赖的接口创建相应的实例,这个工作改由IoC容器在运行时自动完成。我们主要在APS.NET MVC的Controller与应用层的服务实现中使用。

    比如下面的APS.NET MVC Controller示例代码:

    public class AdminController : Controller
    {
    private IBlogSiteManagementService _blogSiteService;
    public AdminController(IBlogSiteManagementService blogSiteService)
    {
    _blogSiteService
    = blogSiteService;
    }
    }

    ASP.NET MVC会在运行时通过IoC容器获取IBlogSiteManagementService的实现,并传递给AdminController的构造函数。(这个操作是在DefaultControllerFactory.Create方法中完成的,参见ASP.NET MVC源代码DefaultControllerFactory.cs第259行)

    注:要让ASP.NET MVC支持依赖注入,需要实现IDependencyResolver接口,并将所有MVC自带的IController的实现注册到你所用的IoC容器。

    自从成功用上了依赖注入,差点成为依赖注入“控”,看到需要依赖的地方,就想着注入。

    爱情是不是也可以依赖注入?在你心中声明一下你心仪的女孩的样子,然后丘比特就会给你注入。。。

    进入正题。。。

    改变博客园团队开发方式的不仅是DDD(领域驱动开发),还有TDD(测试驱动开发)。有了轻微的依赖注入“控”,在写测试代码时,我们不由自主地想到了测试类的构造函数依赖注入。

    可是尝试了几个主流的.NET测试框架(MsTest, NUnit, MbUnit, xUnit.net),连带参数的构造函数都不支持,更别谈构造函数依赖注入。

    搜遍互联网,也没发现有人试图解决这个问题。

    刚开始TDD时,我们就被这个问题困扰,似乎是一个暂时无法解决的问题。。。于是,这就成为了一个可望而不可及的梦想。

    不能注入,那只能在无参构造函数中手动获取所依赖接口的实现实例,示例代码(用的是xUnit.net)如下:

    public class MyPostList
    {
    private IBlogSiteManagementService _blogSiteService;
    public MyPostList()
    {
    _blogSiteService
    = IoCFactory.Instance.CurrentContainter
    .Resolve
    <IBlogSiteManagementService>();
    }
    [Fact]
    public void Get_My_Recent_Admin_Posts()
    {
    Assert.NotNull(_blogSiteService);
    }
    }

    昨天开始,我们决定攻克这个难题,比较了几个测试框架,最终选择了xUnit.net(MsTest由于没有开源,根本没考虑)。

    选择的理由是xUnit.net是NUnit的开发者开发的,扩展性很好。

    解决的思路很简单:找到测试运行时测试类的实例化是在哪进行的。

    有了源代码,让这个寻找过程轻松了很多。

    xUnit.net在Visual Studio中也是通过TestDriven.net加载的,所以打开xUnit的源代码,直奔主题,进入xunit.runner.tdnet项目,打开TdNetRunner.cs,然后顺藤摸瓜:

    Xunit.TestRunner() > Xunit.ExecutorWrapper.RunClass() > 
    Xunit.Sdk.Executor.RunTests() > TestClassCommandRunner.Execute() >
    TestCommandFactory.Make() > LifetimeCommand.Execute()

    在Xunit.Sdk.LifetimeCommand.Execute(object testClass)中发现了瓜:

    public override MethodResult Execute(object testClass)
    {
    if (testClass == null)
    testClass
    = method.CreateInstance();
    }

    method.CreateInstance()实际调用的代码是这样的:

    public object CreateInstance()
    {
    return Activator.CreateInstance(method.ReflectedType);
    }

    一看代码就知道为什么只支持无参的构造函数。

    看到瓜,就容易找到解决方法,既然在 testClass == null 时才会创建测试类的实例,如果我们通过IoC容器在这个方法执行前创建testClass的实例并传递LifetimeCommand.Execute方法,不就可以解决问题了吗?

    所以,继续顺藤摸瓜,找出在哪里调用了LifetimeCommand.Execute(object testClass)方法。。。

    在Xunit.Sdk.TestClassCommandRunner.Execute()中找到:

    MethodResult methodResult = command.Execute(testClassCommand.ObjectUnderTest);

    testClassCommand的类型是ITestClassCommand接口,该接口的实现是Xunit.Sdk.TestClassCommand,看看它的ObjectUnderTest属性:

    public object ObjectUnderTest
    {
    get { return null; }
    }

    只要这里从IoC容器返回测试类的实例,就可以实现依赖注入了。

    进入解决方案。。。

    注:解决方案不需要修改任何xUnit.net的源代码。

    在单元测试项目中创建一个实现ITestClassCommand接口的IocTestClassCommand类,代码如下:

    public class IocTestClassCommand : ITestClassCommand
    {
    TestClassCommand _testClassCommand;

    public IocTestClassCommand()
    :
    this((ITypeInfo)null) {
    }

    public IocTestClassCommand(Type typeUnderTest)
    :
    this(Reflector.Wrap(typeUnderTest)) {
    }

    public IocTestClassCommand(ITypeInfo typeUnderTest)
    {
    _testClassCommand
    = new TestClassCommand(typeUnderTest);
    }

    public object ObjectUnderTest
    {
    get { return IoCFactory.Instance.CurrentContainter.
    Resolve(_testClassCommand.TypeUnderTest.Type); }
    }

    public Random Randomizer
    {
    get { return _testClassCommand.Randomizer; }
    set { _testClassCommand.Randomizer = value; }
    }

    public ITypeInfo TypeUnderTest
    {
    get { return _testClassCommand.TypeUnderTest; }
    set { _testClassCommand.TypeUnderTest = value; }
    }

    [SuppressMessage(
    "Microsoft.Design", "CA1062:Validate arguments of public methods",
    MessageId
    = "0", Justification = "This parameter is verified elsewhere.")]
    public int ChooseNextTest(ICollection<IMethodInfo> testsLeftToRun)
    {
    return _testClassCommand.Randomizer.Next(testsLeftToRun.Count);
    }

    public Exception ClassFinish()
    {
    return _testClassCommand.ClassFinish();
    }

    public Exception ClassStart()
    {
    return _testClassCommand.ClassStart();
    }

    public IEnumerable<ITestCommand> EnumerateTestCommands(IMethodInfo testMethod)
    {
    return _testClassCommand.EnumerateTestCommands(testMethod);
    }

    public IEnumerable<IMethodInfo> EnumerateTestMethods()
    {
    return _testClassCommand.EnumerateTestMethods();
    }

    public bool IsTestMethod(IMethodInfo testMethod)
    {
    return _testClassCommand.IsTestMethod(testMethod);
    }
    }

    红色字体部分就是实现依赖注入的代码,其他都是为了实现ITestClassCommand接口,将方法调用转发给Xunit.Sdk.TestClassCommand。

    关键问题解决,但大功还没造成,还有两个问题需要解决:

    1. 如何使用自己定义的IocTestClassCommand?

    2. 如何向IoC容器注册所依赖接口的实现?

    我们需要定义一个Attribute,继承自Xunit.RunWithAttribute,名叫IoCTestClassCommandAttribute,代码如下:

    public class IoCTestClassCommandAttribute : RunWithAttribute
    {
    public IoCTestClassCommandAttribute()
    :
    base(typeof(IocTestClassCommand))
    {
    IBootStrapper bootStrapper
    = new DefaultBootStrapper();
    bootStrapper.Boot();
    }
    }

    bootStrapper.Boot是为了解决第二个问题,在其中完成IoC容器的注册。BootStrapper是一个独立的项目,ASP.NET MVC项目也是调用这个接口进行IoC容器的注册,这样实现了单元测试项目与Web项目对IoC注册的重用。

    IoCTestClassCommandAttribute在构造函数中将IocTestClassCommand的类型信息传递给父类RunWithAttribute,解决了第一个问题。

    进入激动人心的时刻。。。

    如何在测试类中使用这个特性?

    [IoCTestClassCommand]  
    public class MyPostList
    {
    private IBlogSiteManagementService _blogSiteService;

    public MyPostList(IBlogSiteManagementService blogSiteService)
    {
    _blogSiteService
    = blogSiteService;
    }

    [Fact]
    public void Get_My_Recent_Admin_Posts()
    {
    Assert.NotNull(_blogSiteService);
    }
    }

    只要在测试类上加上[IoCTestClassCommand]属性即可。

    运行测试:

    大功造成!只要坚持,梦想总能成现实!

    进入小结。。。

    单元测试都可以依赖注入,爱情为什么不可以?只要明确自己的依赖,耐心寻找与等待,丘比特会注入属于你的缘分!

  • 相关阅读:
    微信转发或分享朋友圈带缩略图、标题和描述的实现方法
    apache一个IP多个站点的配置方法
    微信网页扫码登录的实现
    laravel take(3) 读取最近三条信息
    微信卡劵、微信卡包,必须是认证订阅号或认证服务号
    CSS3 去除苹果浏览器按钮input[type="submit"]和input[type="reset"]的默认样式
    使用laravel5.4结合easywechat进行微信开发--基本配置
    Class 'QrCode' not found ? 和 laravel 生成二维码接口(Simple QrCod)
    windows redis的启动 和 Laravel中Redis的使用
    改变checkbox的默认样式
  • 原文地址:https://www.cnblogs.com/dudu/p/unit_testing_constructor_dependency_injection.html
Copyright © 2011-2022 走看看