梦想成现实:用xUnit.net在单元测试中实现构造函数依赖注入
来源:广州中睿信息技术有限公司
发布时间:2012/10/21 23:25:16 编辑:admin 阅读 1536

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

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

比如下面的APS。NETMVCController示例代码:

publicclassAdminController:Controller

{

    privateIBlogSiteManagementService_blogSiteService;

    publicAdminController(IBlogSiteManagementServiceblogSiteService)

    {

        _blogSiteService=blogSiteService;

    }

}

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

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

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

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

进入正题。。。

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

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

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

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

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

publicclassMyPostList

{

    privateIBlogSiteManagementService_blogSiteService;

    publicMyPostList()

    {

        _blogSiteService=IoCFactory.Instance.CurrentContainter

        .Resolve<IBlogSiteManagementService>();

    }

    [Fact]publicvoidGet_My_Recent_Admin_Posts()

    {

        Assert.NotNull(_blogSiteService);

    }

}

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

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

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

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

xUnit.net在VisualStudio中也是通过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)中发现了瓜:

publicoverrideMethodResultExecute(objecttestClass)

{

    if(testClass==null)

    testClass=method.CreateInstance();

}

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

publicobjectCreateInstance()

{

    returnActivator.CreateInstance(method.ReflectedType);

}

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

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

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

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

MethodResultmethodResult=command.Execute(testClassCommand.ObjectUnderTest:;

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

public object ObjectUnderTest

{????

    get { return null; }

}

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

进入解决方案...

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

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

publicclassIocTestClassCommand:ITestClassCommand

{

    TestClassCommand_testClassCommand;

    publicIocTestClassCommand()

    :this((ITypeInfo)null){

  }

  publicIocTestClassCommand(TypetypeUnderTest)

  :this(Reflector.Wrap(typeUnderTest)){

  }

  publicIocTestClassCommand(ITypeInfotypeUnderTest)

  {

    _testClassCommand=newTestClassCommand(typeUnderTest);

  }

  publicobjectObjectUnderTest

  {

    get{returnIoCFactory.Instance.CurrentContainter.

    Resolve(_testClassCommand.TypeUnderTest.Type);}

  }

  publicRandomRandomizer

  {

    get{return_testClassCommand.Randomizer;}

    set{_testClassCommand.Randomizer=value;}

  }

  publicITypeInfoTypeUnderTest

  {

    get{return_testClassCommand.TypeUnderTest;}

    set{_testClassCommand.TypeUnderTest=value;}

  }

  [SuppressMessage(“Microsoft.Design”,“CA1062:Validateargumentsofpublicmethods”,

  MessageId=“0”,Justification=“Thisparameterisverifiedelsewhere.”)]

  publicintChooseNextTest(ICollection<IMethodInfo>testsLeftToRun)

  {

    return_testClassCommand.Randomizer.Next(testsLeftToRun.Count);

  }

  publicExceptionClassFinish()

  {

    return_testClassCommand.ClassFinish();

  }

  publicExceptionClassStart()

  {

    return_testClassCommand.ClassStart();

  }

  publicIEnumerable<ITestCommand>EnumerateTestCommands(IMethodInfotestMethod)

  {

    return_testClassCommand.EnumerateTestCommands(testMethod);

  }

  publicIEnumerable<IMethodInfo>EnumerateTestMethods()

  {

    return_testClassCommand.EnumerateTestMethods();}publicboolIsTestMethod(IMethodInfotestMethod)

  {

    return_testClassCommand.IsTestMethod(testMethod);

  }

}

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

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

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

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

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

publicclassIoCTestClassCommandAttribute:RunWithAttribute

{

  publicIoCTestClassCommandAttribute()

  :base(typeof(IocTestClassCommand))

  {

    IBootStrapperbootStrapper=newDefaultBootStrapper();

    bootStrapper.Boot();

  }

}

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

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

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

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

[IoCTestClassCommand]

publicclassMyPostList

{

  privateIBlogSiteManagementService_blogSiteService;

  publicMyPostList(IBlogSiteManagementServiceblogSiteService)

  {

    _blogSiteService=blogSiteService;

  }

  [Fact]

  publicvoidGet_My_Recent_Admin_Posts()

  {

   Assert.NotNull(_blogSiteService);

  }

}

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

运行测试:

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



项目回归测试问题 zhgx2011
测试度量的内容都有哪些 俎涛
对已有的代码,怎么做单元测试 画画公子
性能测试有很多点,怎么识别性能关键点呢 快乐的周末
性能测试都需要监视那些指标? 俎涛
离项目结束多长时间,开始进入贝塔测试? 丹枫
更多问题 ...