Net中的AOP


.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(斷言階段)

  1. Arrange:創建一個被測類的新實例;
  2. Act:給Reverse方法傳入一個字符串並獲得返回結果;
  3. 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使用產生的動態代理 }); 

這種方法有幾個問題:

  1. 首先最明顯的就是美觀問題:將一個切面應用到一個服務類上要寫很多的代碼。
  2. 應該使用一種方法讓StructureMap處理依賴而不是大量的new。
  3. 可能不太明顯,如果想使用一個切面多次呢?如果繼續使用這種方法,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基類。這次會重寫OnEntryOnSuccess方法,並在這兩個方法內輸出日志:

[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。
接下來就該寫執行了,這里實例化一個切面對象,並先后調用OnEntryOnSuccess方法,模擬在運行時切面被使用的時候發生了什么。
最后,會輸出兩個斷言,看看預期的和實際的日志信息是否相同。

[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會在運行時執行該方法,但是在運行時方法如OnEntryOnSuccess方法之前(對LocationInterceptionAspectMethodInterceptionAspect也適用)。重寫該方法,在方法中使用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"); } } 

現在這個切面就可用了,然后,將這個切面以特性的形式用在ServiceOneDoWorkOne上:

   [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時,你會看到它這么強大和靈活所付出的代價。

好文要頂
如果您認為這篇文章還不錯或者有所收獲,您可以通過右邊的“打賞”功能 打賞我一杯咖啡【物質支持】,也可以點擊右下角的【店長推薦】按鈕【精神支持】,因為這兩種支持都是我繼續寫作,分享的最大動力!
作者: tkb至簡


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM