Net中的AOP
.Net中的AOP系列之《单元测试切面》
本篇目录
本节的源码本人已托管于Coding上:点击查看。
本系列的实验环境:VS 2013 Update 5(建议最好使用集成了Nuget的VS版本,VS Express版也够用)。
这节我们说说AOP中的单元测试。单元测试对于保障一款产品的质量还是很重要的,当你写了一个开源的东西,最好要对它进行单元测试通过后再分享,不然别人如何知道你的东西最后会不会出问题;楼主现在从事的一家互联网金融公司也是需要做单元测试的,而且还做了自动化测试(楼主目前主导从事AT这块,以后会分享关于AT的文章),毕竟这都是和大笔资金有关的,不确保产品的质量就上线是不行的,不做测试有时上线产品也是没有自信的,谁也无法确保自己写的代码不出bug,而单元测试和自动化测试都通过后,就会信心十足,虽然还是会出bug。如果你们做的是TDD(测试驱动开发),毫无疑问要写单元测试,这样才能驱动编码的设计和架构。好了,关于测试的话题,以后有机会分享,现在切入今天的正题当使用了AOP后,如何进行单元测试?
使用NUnit编写测试
如果你写过单元测试(UT),那么这篇博客说的东西你应该很熟悉。这儿使用一个.Net单元测试常见的工具NUnit来复习一下单元测试。如果你更喜欢其它的测试工具或框架也是没问题的,仍然可以继续阅读,重要的是思想。
NUnit
NUit是免费开源的,而且具有良好的文档说明,因而收到很多人喜爱。除了NUnit之外,其它的测试框架如MSTest,MSpec,xUnit.net 等都是一些好的测试框架。因此,你喜欢哪个或者使用哪个更顺手就选择哪个吧。
如果你还没有编写UT的习惯,或者从来都没谢过UT,建议你最好尽快学会这门技术,迟早派的上用场的。UT是一个大的话题,因此一篇博客不可能全部覆盖到,因此,建议之后自己阅读一下测试的书籍。
编写和运行NUnit测试
首先创建一个类库项目,取名 UnitTestDemo,之后创建一个string的扩展类MyStringExtension,具体代码如下:
public class MyStringExtension
{
/// <summary>
/// 创建一个反转字符串的方法,比如 输入hello,返回olleh
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public string Reverse(string str)
{
return str;//现在暂时直接返回,为了看看测试的效果
}
}
现在应该准备测试的工具了,使用Nuget安装NUnit:PM> Install-Package NUnit
。安装之后,第一步是写一个Test Fixture(测试装备),它是一个包含了Test的类(可能也包含setup/teardown代码)。使用NUnit编写Test Fixture很简单,只需要在一个类上使用TestFixture
特性就可以了:
[TestFixture]
public class MyStringExtensionTest
{
}
在这个Test Fixture里面,写一个简单的测试验证一下Reverse
方法是否和自己预想的一样。测试一般遵循3A模式,即Arrange(准备阶段), Act(执行阶段), Assert(断言阶段)。
- Arrange:创建一个被测类的新实例;
- Act:给Reverse方法传入一个字符串并获得返回结果;
- Assert:检查字符串是否按预期的那样反转了。
按照3A模式,写出的代码如下:
[TestFixture]
public class MyStringExtensionTest
{
[Test]
public void Reverse_Test()
{
var myStrObj=new MyStringExtension();
var reversedStr = myStrObj.Reverse("hello");
Assert.That(reversedStr,Is.EqualTo("olleh"));//断言语法根据使用的工具和爱好不同可以有很多写法
}
}
因为我们还没有正确实现Reverse方法,所以测试应该是失败的。如果你已经安装了Resharp或者TestDriven.net,那么可以使用这些工具运行测试,楼主已经安装了Resharp,所以可以直接运行测试。当然你可以安装NUnit的一些测试工具。
测试失败的截图如下:
正确实现Reverse方法:
public string Reverse(string str)
{
//return str;//现在暂时直接返回,为了看看测试的效果
return new string(str.Reverse().ToArray());
}
通过的单元测试截图如下:
这时你就开始考虑更多的情况了,比如,如果传入的是null,就返回null,因为Reverse方法没有对null做检查,因此会抛出NullReferenceException,因此我们先写测试用例:
[Test]
public void ReverseWithNull_Test()
{
var myStrObj=new MyStringExtension();
var reversedStr = myStrObj.Reverse(null);
Assert.IsNull(reversedStr);
}
测试结果失败:
在Reverse方法中加入null判断:
public string Reverse(string str)
{
if (string.IsNullOrEmpty(str))
{
return null;
}
//return str;//现在暂时直接返回,为了看看测试的效果
return new string(str.Reverse().ToArray());
}
再次执行测试用例,通过:
切面的测试策略
谈到测试切面时,一般要测两个东西:一是切面是否用在了正确的地方(测试切入点),二是切面是否完成了预期的事情(测试通知)。.Net中的AOP工具通常通过特性使用切面,我们在PostSharp和MVC ActionFilter中看到过了。要测试特性是否用在了正确的地方,只需要测试特性是否用在了我们期望的类和方法上即可。
回忆一下,当在VS中创建了一个 ASP.NET MVC 项目时,会自动创建一个AccountController
,该控制器中的一些方法使用了ValidateAntiForgeryToken
特性(这就是一个ActionFilter)。创建项目时,同时勾选创建单元测试项目复选框时,也会为我们创建一个单元测试的项目,默认使用的微软VS自带的Microsoft.VisualStudio.QualityTools.UnitTestFramework
测试框架。
默认在测试项目中帮我们生成了一个测试Home控制器的类:
假如我们要测试一下那些特性是否处于正确的地方,比如测试一下使用了ValidateAntiForgeryToken
特性的LogOff
方法:
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
return RedirectToAction("Index", "Home");
}
在VS帮我们创建的测试项目中新建一个AccountControllerTest
单元测试类,创建测试用例如下:
[TestMethod]
public void LogOff()
{
var classUnderTest = typeof (AccountController);
var allMethods = classUnderTest.GetMethods();//获得所有方法
var methodUnderTest = allMethods.Where(m => m.Name == "LogOff");//获得LogOff方法
foreach (MethodInfo methodInfo in methodUnderTest)
{
var attribute = Attribute.GetCustomAttribute(methodInfo, typeof (ValidateAntiForgeryTokenAttribute));//寻找方法上的ValidateAntiForgeryTokenAttribute特性
Assert.IsNotNull(attribute);//如果存在,测试通过
}
}
通过微软自带的测试框架写的测试类,在测试项目生成之后,会在测试资源管理器中出现所有可以运行的测试方法:
运行测试用例,通过:
上面的代码使用了System.Reflection
来获取类,方法,方法上的特性,然后断言特性是否为null。注意,这个测试不是为了测试ValidateAntiForgeryTokenAttribute
特性做了什么,而是它是否出现在正确的地方。
有些人可能认为这样做太冗余了或者这样做过犹不及,他们也都有似乎合理的理由,但是在一个大的团队或者项目中,有时很难跟踪应该使用哪些切面,并且这些很容易忘记,所以这些类型的测试也是很有用的。
对于像使用了IoC容器而不是特性的DynamicProxy来说,测试切面是否存在稍微有些不同。如果你已经编写了测试来验证选择的IoC工具是否初始化正确,那么也应该同时测试动态代理。
对切面编写测试可能因工具选择的不同而不同,因开发者、框架不同而不同,因此变化可能很大,本系列教程主要使用Castle DynamicProxy和PostSharp来讲解。
Castle DynamicProxy测试
使用Castle DynamicProxy写的切面只实现了IInterceptor
接口,其它方面,就像一个普通的类:编译、实例化、在运行时执行(这和PostSharp切面不同,后者在编译时实例化,并在编译时执行部分代码)。因此,测试使用Castle DynamicProxy的切面就像测试POCO类一样简单。
首先看一下如何测试一个最简单切面,该切面是自我包含的且没有任何依赖。然后看一下如何测试使用DI解析依赖的简单切面。如果你熟悉DI在单元测试中扮演的角色,那么测试DynamicProxy类应该很熟悉。
测试一个拦截器
假设有个拦截器,使用了静态的Log类在方法执行前后输出一些信息。首先创建一个静态Log类存储字符串,如下:
public static class Log
{
private static List<string> _messages=new List<string>();
public static List<string> Messages
{
get { return _messages; }
}
public static void Write(string message)
{
_messages.Add(message);
}
}
下一步,写一个使用了这个类的拦截器。并在拦截的方法执行前后输出一些信息:
public class MyInterceptor:IInterceptor
{
public void Intercept(IInvocation invocation)
{
Log.Write(invocation.Method.Name+"执行前");
invocation.Proceed();
Log.Write(invocation.Method.Name+"执行后");
}
}
之前已经看到过使用Castle DynamicProxy写的切面了,但如何测试呢?既然拦截器是一个常规的类,那就可以实例化一个对象,然后调用它的Intercept方法,然后检查一下记录的日志是否和预期的一样。顺着这个思路,写出的代码如下:
[TestFixture]
public class MyInterceptorTest
{
[Test]
public void TestIntercept()
{
var myInterceptor=new MyInterceptor();
IInvocation invocation;//这里先不赋值,下面接着说
myInterceptor.Intercept(invocation);
Assert.IsTrue(Log.Messages.Contains(invocation.Method.Name+"执行前"));
Assert.IsTrue(Log.Messages.Contains(invocation.Method.Name+"执行后"));
}
}
因为上面的invocation变量没有赋值,所以编译是不通过的。如果你之前做过单元测试的话,那么你也应该知道单元测试中有这么一个概念:伪造【mocking】。
当拦截器在程序中运行时,DynamicProxy会创建invocation对象,它对于测试来说是隔离的,因此我们必须伪造一个对象来模拟真正的invocation对象,伪造的目的仅仅是为了测试。为了达到这个目的,这里使用了一个伪造工具Moq,虽然还有很多可以用,但是这里使用Moq作为示例。使用Nuget安装Moq:PM> Install-Package Moq
。
现在,创建一个实现了IInvocation
接口的伪造对象,然后将它传给Intercept
方法,因为Intercept方法只关心invocation.Method.Name
,所以只需要给那个伪造对象定义那个属性就可以了,Moq会给其它属性设置默认值:
[Test]
public void TestIntercept()
{
var myInterceptor=new MyInterceptor();
//IInvocation invocation;//这里先不赋值,下面接着说
var mockedInvocation=new Mock<IInvocation>();
mockedInvocation.Setup(m => m.Method.Name).Returns("MyMethod");//Arrange:将被拦截的方法的Name属性设置为MyMethod
var invocation = mockedInvocation.Object;//使用Object属性获得要传入的真实对象
myInterceptor.Intercept(invocation);
Assert.IsTrue(Log.Messages.Contains(invocation.Method.Name+"执行前"));
Assert.IsTrue(Log.Messages.Contains(invocation.Method.Name+"执行后"));
}
现在可以编译运行了,当运行该测试时,测试应该通过。如果进一步看一下Moq的话,你就会发现Moq本身使用了DynamicProxy。因此,在一定程度上,我们使用了DynamicProxy切面测试其它的DynamicProxy切面,这是没有任何问题的,因为我们不是在测试框架本身而是使用框架生成的代码。测试结果如下:
注入依赖
真实项目中,使用上面的静态Log类会造成Log类和任何用到它的地方之间紧耦合,因此,应该使用logging接口,并隐藏实现细节。和写切面是一样的:你想将依赖传入MyInterceptor
类中。切面和其它模块是一样的,应该遵守依赖反转原则,应该依赖抽象而不是实现。
加入IoC
IoC工具在一个复杂点的例子里会实用点,因此下面创建一个比之前复杂的例子。见下面的案例图:
实现服务
创建一个控制台项目CastleDynamicProxyUT,添加NUnit,Castle.Core,StructureMap。注意这里安装的StructureMap版本是Install-Package structuremap -Version 2.6.4.1
。
下面,按照上图从下到上实现,创建接口IServiceTwo和它的实现ServiceTwo,里面添加一个方法DoWorkTwo
,这里仅仅作为演示,具体该方法中有什么代码不重要。
命名惯例
这里使用的是ServiceName和IServiceName的命名惯例,因为这是StructureMap使用的默认惯例。当配置依赖时,只要遵守了这个惯例,就不必显式列出每个接口/实现对。
public interface IServiceTwo
{
void DoWorkTwo();
}
public class ServiceTwo:IServiceTwo
{
public void DoWorkTwo()
{
throw new System.NotImplementedException();
}
}
下一步,创建LoggingService的实现和接口。真实项目中,这个服务都会使用NLog,log4net等等,但这里为了简单演示,只将日志输出到控制台,这个日志服务可能会用在项目中的任何地方,但是通过logging切面使用的。
public interface ILoggingService
{
void Write(string message);
}
public class LoggingService : ILoggingService
{
public void Write(string message)
{
Console.WriteLine("Logging:"+message);
}
}
编写Logging切面
该切面依赖LoggingService,通过构造函数注入可以获得ILoggingService依赖。在Intercept
方法中,它在拦截的方法执行前后分别输出“Log start”和“Log end”:
public class LoggingAspect:IInterceptor
{
private readonly ILoggingService _loggingService;
public LoggingAspect(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public void Intercept(IInvocation invocation)
{
_loggingService.Write("Log start");
invocation.Proceed();
_loggingService.Write("Log end");
}
}
对切面进行单元测试
上面的切面不像之前的测试那样简单,因为这个切面多个依赖。我们只想测试切面,不想测试依赖,因此需要使用伪造工具创建一个代替对象传入LoggingAspect
构造函数,这样就可以独立地测试切面了,记得要安装Moq。
[TestFixture]
public class LoggingAspectTest
{
[Test]
public void TestIntercept()
{
var mockedLoggingService=new Mock<ILoggingService>();//为ILoggingService创建一个伪造对象
var loggingAspect=new LoggingAspect(mockedLoggingService.Object);//使用伪造对象的Object属性实例化LoggingAspect
var mockedInvocation=new Mock<IInvocation>();//为IInvoation对象创建一个伪造对象
loggingAspect.Intercept(mockedInvocation.Object);
mockedLoggingService.Verify(x=>x.Write("Log start"));//使用伪造对象的Verify验证Write方法是否像期待的那样执行
mockedLoggingService.Verify(x=>x.Write("Log end"));
}
}
测试DynamicProxy的切面是很容易的,但我们还没有看到全局,因此,继续按照示意图完成其它依赖的代码。这个切面需要拦截ServiceOne的任何调用。
创建ServiceOne
创建ServiceOne实现和接口。这个服务没有做太多的事情,只是输出到控制台,示意图上说明它会依赖ServiceTwo接口,因此在构造函数中要确保它传入,虽然传入了依赖,但为了演示目的,这里并没有真正使用该依赖:
public interface IServiceOne
{
void DoWorkOne();
}
public class ServiceOne:IServiceOne
{
public ServiceOne(IServiceTwo serviceTwo)
{
//虽然没有使用IServiceTwo依赖,但是没有它,ServiceOne是不能实例化的
}
public void DoWorkOne()
{
Console.WriteLine("ServiceOne's DoWorkOne finished the execution!");
}
}
随着例子越来越复杂,StructureMap就会派上用场了。在没使用StructureMap之前,先来看看没有IoC工具时程序如何使用ServiceOne。要在Main方法中使用ServiceOne,因为它依赖ServiceTwo,所以必须先要实例化ServiceTwo:
class Program
{
static void Main(string[] args)
{
#region 1.0 不使用StructureMap的情况
var service2=new ServiceTwo();
var service1=new ServiceOne(service2);
service1.DoWorkOne();
#endregion
}
}
运行程序的话,就会在控制台看到“ServiceOne's DoWorkOne finished the execution!”。
使用IOC工具管理依赖
代码执行结果看起来没问题,但是在Main方法中依赖了特定的实现new ServiceTwo(),new ServiceOne(service2)
违反了依赖反转原则,这会造成这两个服务类和Program类紧耦合。从架构设计的角度来说这是一个设计缺陷,而且想象一下如果有一个更复杂的依赖关系图呢:每次调用一个服务上的一个方法时,你可能都要花费5行以上的代码实例化所有的对象。
对于这种情况,我们应该使用StructureMap管理依赖,并实例化正确的服务。这样,就不用来new特定的实现了,只需要命令StructureMap完成某个接口的实现就可以了。下面看一下使用默认惯例的StructureMap的基本配置:
#region 2.0 使用StructureMap
ObjectFactory.Initialize(config =>//不同的IOC工具初始化代码是不同的
{
config.Scan(scanner =>
{
scanner.TheCallingAssembly();
scanner.WithDefaultConventions();//使用默认的惯例
});
});
var service1 = ObjectFactory.GetInstance<IServiceOne>();
service1.DoWorkOne();
运行程序,会看到和之前一样的输出,但是这次StructureMap会帮我们处理依赖图中的所有依赖连接。
DynamicProxy和StructureMap结合
前面已经知道,需要使用ProxyGenerator可以将一个DynamicProxy切面应用到一个类上。前面几篇博客中,我们都是在StructureMap的配置中处理的,但是ServceOne有一个依赖,因此比之前更复杂了。
StructureMap自带的拦截
如果你熟悉StructureMap,那么你应该知道它有自己的拦截能力,比如InstanceInterceptor
接口。对于确定类型的装饰器,这个工具够用了,但是DynamicProxy有个更强大的拦截工具,所以这里不使用StructureMap的InstanceInterceptor。
一种方法是实例化切面和它的依赖,实例化服务类和它的依赖,这样就可以将切面应用到服务上了:
ObjectFactory.Initialize(config =>//不同的IOC工具初始化代码是不同的
{
config.Scan(scanner =>
{
scanner.TheCallingAssembly();
scanner.WithDefaultConventions();//使用默认的惯例
});
var proxyGenerator = new ProxyGenerator();
var aspect = new LoggingAspect(new LoggingService());
var service = new ServiceOne(new ServiceTwo());
var result = proxyGenerator.CreateInterfaceProxyWithTargetInterface(typeof(IServiceOne), service, aspect);//应用切面
config.For<IServiceOne>().Use((IServiceOne) result);//告诉StructureMap使用产生的动态代理
});
这种方法有几个问题:
- 首先最明显的就是美观问题:将一个切面应用到一个服务类上要写很多的代码。
- 应该使用一种方法让StructureMap处理依赖而不是大量的new。
- 可能不太明显,如果想使用一个切面多次呢?如果继续使用这种方法,StructureMap初始化可能会变得非常凌乱。
使用EnrichWith重构
幸运的是,我们可以结合一个helper类和StructureMap的叫做EnrichWith的功能来精简代码。StructureMap的EnrichWith方法可以用于注册一个方法来代替正常服务的对象,就像注入一个拦截器的最佳地方。下面将大部分的凌乱代码放到EnrichWith语句中:
#region 3.0 使用EnrichWith重构
ObjectFactory.Initialize(config =>
{
config.Scan(scanner =>
{
scanner.TheCallingAssembly();
scanner.WithDefaultConventions();
});
var proxyGenerator = new ProxyGenerator();
config.For<IServiceOne>().Use<ServiceOne>().EnrichWith(svc =>
{
var aspect = new LoggingAspect(new LoggingService());
var result = proxyGenerator.CreateInterfaceProxyWithTargetInterface(typeof(IServiceOne), svc, aspect);
return result;
});
});
#endregion
比之前的代码好多了,但是每次使用一个切面仍然要输入很多东西。进一步优化,我们可以把EnrichWith
里的代码尽可能多地封装到可复用的代理创建类里,最好像下面的代码那样:
ObjectFactory.Initialize(config =>
{
config.Scan(scanner =>
{
scanner.TheCallingAssembly();
scanner.WithDefaultConventions();
});
var proxyHelper = new ProxyHelper();
//注意Proxify方法本身以实参传入EnrichWith方法
config.For<IServiceOne>().Use<ServiceOne>().EnrichWith(proxyHelper.Proxify<IServiceOne, LoggingAspect>);
});
上面的代码更加简洁,使用EnrichWith方法只用到了proxyHelper的Proxify方法,服务接口和切面类。
使用ProxyHelper
上面我们已经看到了这个类,只需要将代理生成器中的代码放到这个类中就可以了。在这个帮助类中,会使用ObjectFactory来解析拦截器对象。
public class ProxyHelper
{
private readonly ProxyGenerator _proxyGenerator;
public ProxyHelper()
{
_proxyGenerator = new ProxyGenerator();//ProxyGenerator移到helper类中
}
public object Proxify<I, A>(object obj) where A : IInterceptor//约束A只允许IInterceptor类型实参
{
var interceptor = (IInterceptor) ObjectFactory.GetInstance<A>();//StructureMap处理切面的依赖
var result = _proxyGenerator.CreateInterfaceProxyWithTargetInterface(typeof (I),obj,interceptor);
return result;
}
}
这小节的主题是对使用DynamicProxy写的切面进行单元测试,所以关于IOC的知识及优化大家可以自己去研究。研究出来的结果就是对使用DynamicProxy写的切面进行单元测试并不是很难。下面一节,我们会对使用PostSharp写的切面进行单元测试。
PostSharp测试
使用PostSharp编写的切面继承自抽象基类,比如OnMethodBoundaryAspect
。它们也是特性,存储在元数据中,因此,没有PostSharp这个postcompiler(后编译,就是代码编译之后再加工)工具,这些特性什么都不会做,也不会执行。该后编译工具会在编译时实例化切面类,序列化,然后再反序列化。因此,直接测试这些切面类是很困难的,在某些情况下,由于后编译编织的本质和PostSharp框架写入的方式直接进行测试根本是行不通的。
对PostSharp切面进行单元测试
之前我们创建了一个静态的Log类,这次也一样,但切面类是不同的:它继承自PostSharp的OnMethodBoundaryAspect
基类。这次会重写OnEntry
和OnSuccess
方法,并在这两个方法内输出日志:
[Serializable]
public class MyBoundaryAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Log.Write("Before:"+args.Method.Name);
}
public override void OnSuccess(MethodExecutionArgs args)
{
Log.Write("After:" + args.Method.Name);
}
}
对于上面类的单元测试和之前的很相似,如下,我们不需要使用Moq,可以直接实例化一个MethodExecutionArgs
对象,该对象的构造函数期望一个实例对象和一个参数列表,但因为这里MyBoundaryAspect
用不到这些,我们分别使用null和Arguments.Empty
代替。切面类使用了Method
属性,因此我们需要将它设置为实现了MethodBase
的某个对象,通过使用System.Reflection提供的DynamicMethod
对象,可以很方便地达到目的。对于测试,这里只关心方法名,因此返回类型和参数类型可以设置为null。
接下来就该写执行了,这里实例化一个切面对象,并先后调用OnEntry
和OnSuccess
方法,模拟在运行时切面被使用的时候发生了什么。
最后,会输出两个断言,看看预期的和实际的日志信息是否相同。
[TestFixture]
public class TestMyLoggerCrossCutConcern
{
[Test]
public void TestMyBoundaryAspect()
{
//Arrange 准备阶段
var args = new MethodExecutionArgs(null, Arguments.Empty);
args.Method = new DynamicMethod("Farb", null, null);
//Act 执行阶段
var aspect = new MyBoundaryAspect();
aspect.OnEntry(args);
aspect.OnSuccess(args);
//Assert 断言阶段
Assert.IsTrue(Log.Messages.Contains("Before:" + args.Method.Name));
Assert.IsTrue(Log.Messages.Contains("After:" + args.Method.Name));
}
}
当然,也可以使用伪造工具创建一个代替MethodExecutionArgs
的对象,但因为它不是一个接口或抽象类,所以必须使用一个更高级的伪造工具,如TypeMock,Moq不能实现这个。下面复习一下DynamicProxy中的复杂例子,看看使用PostSharp会有什么不同。
注入依赖
之前的例子使用了StructureMap结合DynamicProxy,依赖通过构造函数注入,使用了PostSharp,切面构造函数会在编译时调用,这个过程在StructureMap初始化之前。因此,构造函数注入是没用的,这就意味着测试更加困难。为了解决这个问题,这里用到了服务定位器模式。
服务定位器是依赖反转的一种形式,与通过构造函数传入服务相反,它会去寻找服务。有时人们认为服务定位模式是反模式,但是,它确实好于压根不用依赖反转。
这一小节,创建一个控制台项目PostSharpUT,安装PostSharp和NUnit,StructureMap,复习一下和之前一样复杂的依赖。
class Program
{
static void Main(string[] args)
{
ObjectFactory.Initialize(x =>
{
x.Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
});
});
var myObj = ObjectFactory.GetInstance<IServiceOne>();
myObj.DoWorkOne();
}
}
服务类和接口与之前的保持不变,IServiceOne , ServiceOne ,
IServiceTwo , ServiceTwo , ILoggingService ,LoggingService直接使用之前项目中的。
LoggingAspect现在继承了OnMethodBoundaryAspect
基类,而不是Castle的IInterceptor
接口,里面使用了ILoggingService的实现,因此应该使用一个私有字段:
[Serializable]
public class LoggingAspect:OnMethodBoundaryAspect
{
private readonly ILoggingService _loggingService;
public LoggingAspect(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public override void OnEntry(MethodExecutionArgs args)
{
_loggingService.Write("Log start");
}
public override void OnSuccess(MethodExecutionArgs args)
{
_loggingService.Write("Log end");
}
}
PostSharp构造器只能以特性的形式使用,C#特性构造器只接受静态值,因此不能像上面那样注入LoggingService依赖。但是可以使用还没有提到的一个PostSharp API,这就是RuntimeInitialize
方法。PostSharp会在运行时执行该方法,但是在运行时方法如OnEntry
和OnSuccess
方法之前(对LocationInterceptionAspect
和MethodInterceptionAspect
也适用)。重写该方法,在方法中使用StructureMap作为服务定位器来初始化_loggingService
。也需要将_loggingService使用特性标记为NonSerialized
,因为直到该切面反序列化之后它才会被初始化。
[Serializable]
public class LoggingAspect:OnMethodBoundaryAspect
{
[NonSerialized]
private ILoggingService _loggingService;
public override void RuntimeInitialize(MethodBase method)
{
_loggingService = ObjectFactory.GetInstance<ILoggingService>();
}
public override void OnEntry(MethodExecutionArgs args)
{
_loggingService.Write("Log start");
}
public override void OnSuccess(MethodExecutionArgs args)
{
_loggingService.Write("Log end");
}
}
现在这个切面就可用了,然后,将这个切面以特性的形式用在ServiceOne
的DoWorkOne
上:
[LoggingAspect]
public void DoWorkOne()
{
Console.WriteLine("ServiceOne's DoWorkOne finished the execution!");
}
执行结果:
对上面的代码进行单元测试就需要多做点工作了。创建一个测试类和测试方法,和之前一样,在测试中,需要创建ILoggingService
的一个Mock对象(如果没有安装Moq先要使用Nuget安装Moq)。再创建一个要传入的MethodExecutionArgs
对象,它不需要有Method属性,因为我们这次没有用到它:
[TestFixture]
public class MyLoggingAspectTest
{
[Test]
public void TestIntercept()
{
var mockedLoggingService=new Mock<ILoggingService>();
var args=new MethodExecutionArgs(null,Arguments.Empty);
}
}
如果使用的是Castle,我们接下来就要实例化切面对象,然后将mockedLoggingAspect对象传给构造函数,但如果使用了PostSharp,就不能那么做了。做法是必须将mockedLoggingAspect对象传给StructureMap,让它实例化切面对象,然后执行RuntimeInitialize
方法,它会向StructureMap请求ILoggingService对象:
[TestFixture]
public class MyLoggingAspectTest
{
[Test]
public void TestIntercept()
{
var mockedLoggingService = new Mock<ILoggingService>();
var args=new MethodExecutionArgs(null,Arguments.Empty);
ObjectFactory.Initialize(x =>
x.For<ILoggingService>().Use(mockedLoggingService.Object));
var loggingAspect=new LoggingAspect();
loggingAspect.RuntimeInitialize(null);
loggingAspect.OnEntry(args);
loggingAspect.OnSuccess(args);
}
}
当OnEntry方法执行时,我们期望loggingService调用Write方法,并输出含有“Log Start”的信息。同样,当执行OnSuccess方法时,我们期望输出含有“Log end”的信息。下面根据单元测试的3A法则,应该验证我们的预期和实际是否相符了:
mockedLoggingService.Verify(x=>x.Write("Log start"));
mockedLoggingService.Verify(x=>x.Write("Log end"));
执行该测试,测试会通过。这次我们也实现了和之前使用Castle DynamicProxy相似级别的测试,只不过做的事情多了些罢了。
别急,好戏还在下面。
PostSharp和测试的问题
当对PostSharp切面做单元测试时,你会面临很多问题。
第一个问题是PostSharp在编译时编织。对后来会修改的代码测试变得复杂。
编译时编织
考虑下面的代码段:
public class MyStringExtension
{
public string Reverse(string str)
{
return new string(str.Reverse().ToArray());
}
}
[TestFixture]
public class MyStringExtensionTest
{
[Test]
public void Reverse_Test()
{
var myStrObj=new MyStringExtension();
var reversedStr = myStrObj.Reverse("hello");
Assert.That(reversedStr,Is.EqualTo("olleh"));
}
}
这是本文开头的例子,传入字符串变量“hello”,然后返回反转后的字符串“olleh”。现在,思考相同的PostSharp切面LoggingAspect
应用到该方法会怎样。
public class MyStringExtension
{
[LoggingAspect]
public string Reverse(string str)
{
return new string(str.Reverse().ToArray());
}
}
在运行时执行的Reverse方法现在会在LoggingAspect类的代码中执行。因此,RuntimeInitialize方法会被执行,然后切面使用StructureMap获得ILoggingService的依赖。现在,Reverse_Test就会变得有点复杂了。我们需要再次伪造ILoggingService,并且初始化StructureMap来获得可代替的对象,因为这是个单元测试,我们对测试logging不感兴趣,只对Reverse
方法感兴趣。
[Test]
public void Reverse_Test()
{
var mockloggingService=new Mock<ILoggingService>();
ObjectFactory.Initialize(x=>
x.For<ILoggingService>().Use(mockloggingService.Object));
var myStrObj=new MyStringExtension();
var reversedStr = myStrObj.Reverse("hello");
Assert.That(reversedStr,Is.EqualTo("olleh"));
}
虽然已经使用了切面完成了漂亮的关注点分离(反转字符串的类和logging类),但运行单元测试时,它们仍然是紧耦合的,因此仍然需要做些额外的工作来分离测试中的伪造对象。此外,编写UT时,需要伪造的服务类是不明显的,因为它不是唯一要实例化切面的。如果忘了一个,那么测试就会失败,因为StructureMap会抛异常。
最后一个是服务定位器的问题,在这个demo中,直接把ObjectFactory.Initialize
放在单元测试中不是问题,因为只有一个UT,但是如果这是个静态方法,当写多个UT时,就必须关心共享状态。比如,当在ObjectFactory中初始化ILoggingService的伪造对象时,该伪造对象会为每个UT保持注册。解决方案就是在你的代码(单元测试,RuntimeInitialize)和StructureMap之间添加一层处理逻辑。这会让UT花费更多功夫。
总之,当编写涉及PostSharp的UT时,很困难。虽然收获了将横切关注点分离到不同的类中的好处,但UT必须做些特殊的处理代码。使用Castle DynamicProxy时不会出现这个问题。
关闭PostSharp的变通方法
PostSharp可以通过VS中的项目属性设置进行关闭,可以临时关闭PostSharp,运行单元测试,然后当测试通过后再打开即可。这种方法几乎不理想,感兴趣的,你可以试试。
另一种变通是使用编译器宏指令。比如,你自定义了一个指令UnitTesting
,就可以使用#if
语句包裹切面代码,如果UnitTesting
指定定义了的话,就会编译一个空切面,这就是说,你可以不需要额外的伪造就可以运行UT了。
#define UnitTesting
[Serializable]
public class LoggingAspect:OnMethodBoundaryAspect
{
#if !UnitTesting
[NonSerialized]
private ILoggingService _loggingService;
public override void RuntimeInitialize(MethodBase method)
{
_loggingService = ObjectFactory.GetInstance<ILoggingService>();
}
public override void OnEntry(MethodExecutionArgs args)
{
_loggingService.Write("Log start");
}
public override void OnSuccess(MethodExecutionArgs args)
{
_loggingService.Write("Log end");
}
#endif
}
}
[Test]
public void Reverse_Test()
{
var myStrObj=new MyStringExtension();//这个UT就不需要关心伪造对象了
var reversedStr = myStrObj.Reverse("hello");
Assert.That(reversedStr,Is.EqualTo("olleh"));
}
这个办法也几乎不理想,你必须通过定义UnitTesting
指定来打开和关闭PostSharp(或者至少找到一种自动化方式)。还有,必须用#if/#end
来包围切面类的所有代码。
一个相似的选择是定义一个全局变量来指示切面代码是否应该运行。这个变量默认是true,但在UT中,可以设置为false。
public static class AspectSettings
{
public static bool On = true;
}
[Serializable]
public class LoggingAspect2:OnMethodBoundaryAspect
{
[NonSerialized]
private ILoggingService _loggingService;
public override void RuntimeInitialize(MethodBase method)
{
if(!AspectSettings.On) return;
_loggingService = ObjectFactory.GetInstance<ILoggingService>();
}
public override void OnEntry(MethodExecutionArgs args)
{
if (!AspectSettings.On) return;
_loggingService.Write("Log start");
}
public override void OnSuccess(MethodExecutionArgs args)
{
if (!AspectSettings.On) return;
_loggingService.Write("Log end");
}
}
[Test]
public void Reverse_Test()
{
AspectSettings.On = false;//关闭设置
var myStrObj=new MyStringExtension();
var reversedStr = myStrObj.Reverse("hello");
Assert.That(reversedStr,Is.EqualTo("olleh"));
}
这种变通可能是最简单的方法了,因为不必担心伪造、共享状态,服务定位器问题或者其他问题了。当测试时,切面会关闭。这仍然不方便,因为必须切面的设置,以确保所有的UT都关闭了切面。
不可访问的构造函数
如果上面所有的问题你觉得都不是问题,那么还有一个问题。
本文的例子中,使用的是OnMethodBoundaryAspect
。使用的参数类是MethodExecutionArgs
,幸运地是它有一个公共的构造函数。另外两个PostSharp切面基类(位置拦截和方法拦截)使用了LocationInterceptionArgs和MethodInterceptionArgs,它们都没有公共的构造函数。这使得创建伪造或者可代替的对象更加困难,你可以使用更高级的伪造工具,如TypeMock(不免费)。
间接测试PostSharp
该说的都说了,该做的也都做了,可能不值得花费精力直接测试PostSharp切面类。应该做的就是保持切面中的代码最小化。切面可能只包含实例化和执行其他类的代码,也就是说,你可以在PostSharp切面类和执行横切关注点的代码之间创建一个间接层。下图展示了PostSharp的例子,但相同的原则也可以用于DynamicProxy或任何其他的切面框架。
一定程度上,这种方式和MVP模式很相似。View是PostSharp切面本身,Presenter是处理工作的分离的类(logging之前,logging之后等等)。切面中的代码极少,因为已经将它的工作委托给一个横切关注点对象(concern)。该横切关注点对象是一个POCO,比如,一个没有继承自PostSharp基类的对象更容易测试。
public class MyNormalCode
{
[MyThinAspect]
public string Reverse(string content)
{
return new string(content.Reverse().ToArray());
}
}
[Serializable]
public class MyThinAspect:OnMethodBoundaryAspect
{
private IMyCrossCuttingConcern _concern;//该切面只有一个StructureMap提供的IMyCrossCuttingConcern依赖
public override void RuntimeInitialize(MethodBase method)
{
if(!AspectSettings.On) return;
_concern = ObjectFactory.GetInstance<IMyCrossCuttingConcern>();
}
public override void OnEntry(MethodExecutionArgs args)
{
if (!AspectSettings.On) return;
_concern.BeforeMethod("before");//委托给BeforeMethod方法
}
public override void OnSuccess(MethodExecutionArgs args)
{
if (!AspectSettings.On) return;
_concern.AfterMethod("after");//委托给AfterMethod方法
}
}
public interface IMyCrossCuttingConcern
{
void BeforeMethod(string logMsg);
void AfterMethod(string logMsg);
}
所有通知代码可以放到IMyCrossCuttingConcern
的实现中:
public class MyCrossCuttingConcern:IMyCrossCuttingConcern
{
private ILoggingService _loggingService;
public MyCrossCuttingConcern(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public void BeforeMethod(string logMsg)
{
_loggingService.Write(logMsg);
}
public void AfterMethod(string logMsg)
{
_loggingService.Write(logMsg);
}
}
MyCrossCuttingConcern很容易测试,因为它和任何AOP框架都不是紧耦合的,构造函数注入再次变得可行。
小结
谈到UT时,Castle DynamicProxy有明显优势,PostSharp的UT至少处于中级难度并且要求更多的代码。
好的软件架构绝大多数都知道做出正确的权衡,并且基于软件的类型和开发目标变化也很灵活。
如果你认为UT很重要,那么不要完全依赖PostSharp。后面,我们还会看一下PostSharp可以提供一些运行时编织工具不能提供的测试形式。PostSharp提供了编译时验证和架构验证,这些都是在编译时发生的。比如,可以使用PostSharp在架构级验证代码(这样,确保所有的NHibernate实体属性都正确地定义为virtual
)。
本文开始暴露的一点是运行时编织工具(Castle DynamicProxy)和后编译时编织工具(PostSharp)有很大的不同。如果只看本文的开头部分,你会看到PostSharp更强大、更灵活,但是最后涉及到UT时,你会看到它这么强大和灵活所付出的代价。
Net中的AOP的更多相关文章
- .Net中的AOP系列之构建一个汽车租赁应用
返回<.Net中的AOP>系列学习总目录 本篇目录 开始一个新项目 没有AOP的生活 变更的代价 使用AOP重构 本系列的源码本人已托管于Coding上:点击查看. 本系列的实验环境:VS ...
- .Net中的AOP读书笔记系列之AOP介绍
返回<.Net中的AOP>系列学习总目录 本篇目录 AOP是什么? Hello,World! 小结 本系列的源码本人已托管于Coding上:点击查看,想要注册Coding的可以点击该连接注 ...
- .Net中的AOP系列之《单元测试切面》
返回<.Net中的AOP>系列学习总目录 本篇目录 使用NUnit编写测试 编写和运行NUnit测试 切面的测试策略 Castle DynamicProxy测试 测试一个拦截器 注入依赖 ...
- .Net中的AOP系列之《拦截位置》
返回<.Net中的AOP>系列学习总目录 本篇目录 位置拦截 .Net中的字段和属性 PostSharp位置拦截 真实案例--懒加载 .Net中的懒加载 使用AOP实现懒加载 如何懒加载字 ...
- .Net中的AOP系列之《方法执行前后——边界切面》
返回<.Net中的AOP>系列学习总目录 本篇目录 边界切面 PostSharp方法边界 方法边界 VS 方法拦截 ASP.NET HttpModule边界 真实案例--检查是否为移动端用 ...
- .Net中的AOP系列之《间接调用——拦截方法》
返回<.Net中的AOP>系列学习总目录 本篇目录 方法拦截 PostSharp方法拦截 Castle DynamicProxy方法拦截 现实案例--数据事务 现实案例--线程 .Net线 ...
- 转-Spring Framework中的AOP之around通知
Spring Framework中的AOP之around通知 http://blog.csdn.net/xiaoliang_xie/article/details/7049183 标签: spring ...
- 【转】在.Net中关于AOP的实现
原文地址:http://www.uml.org.cn/net/201004213.asp 一.AOP实现初步 AOP将软件系统分为两个部分:核心关注点和横切关注点.核心关注点更多的是Domain Lo ...
- Spring中的AOP
什么是AOP? (以下内容来自百度百科) 面向切面编程(也叫面向方面编程):Aspect Oriented Programming(AOP),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种 ...
随机推荐
- Android_menu_optionMenu
xml文件:<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns: ...
- Sqlserver的触发器的简单使用
1,触发器有两种 (1)After触发器(之后触发) 触发器有个好处:就是你之前有过什么操作他会将你的操作的数据信息完整的保存下来,比如你删过什么信息,如果用触发器,那么删除后就会显示两行受影响,那么 ...
- WebSocket原理及与http1.0/1.1 long poll和 ajax轮询的区别【转自知乎】
一.WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先HTTP有1.1和1.0之说,也就是所谓的ke ...
- CyclicBarrier 使用说明
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行.叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用. 主要方法: public i ...
- SWT中的布局之-----FormLayout(表格式布局)
表格式(FormLayout类) 表格式布局管理器,通过创建组件各个边的距离来布局组件,和GridLayout一样强大. 用GridLayout与FormLayout都可以实现相同的界面效果,但有时使 ...
- Linux服务器命令行模式安装Matlab2014a
Linux服务器命令行模式安装Matlab2014a,有需要的朋友可以参考下. 0.下载安装包 下载Matlab2014a for Linux安装包的ISO镜像文件(感谢万能的度娘)以及破解包(下载地 ...
- 序列化和反序列化(C#)
有时候我们希望把类的实例保存下来,以便以后的时候用.一个直观的方法就是StreamWriter把类写成一行,用\t分隔开每个属性,然后用StreamReader读出来. 但是这样太麻烦,代码行数较多, ...
- Android数据存储方式之SharedPreferences
Android平台给我们提供了一个SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数.使用SharedPreferences保存数据,其背后是用xml文件存放数 ...
- 利用openssl进行RSA加密解密
openssl是一个功能强大的工具包,它集成了众多密码算法及实用工具.我们即可以利用它提供的命令台工具生成密钥.证书来加密解密文件,也可以在利用其提供的API接口在代码中对传输信息进行加密. RSA是 ...
- 0708_Java如何设置输入流
1.Java如何设置输入流:?(以解决看下面实例代码) 2.Java如何设置全局变量:(以解决public static即可) 3.Java为什么在做那种机试题目的时候都要设置成静态的:(以解决,因为 ...