<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

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

      posted @ 2016-10-23 10:00  tkbSimplest  閱讀(6494)  評論(8)    收藏  舉報
      主站蜘蛛池模板: 国产一区二区三区小说 | 国内不卡一区二区三区| 国产精品久久久久无码网站| 无码囯产精品一区二区免费 | 日韩欧美不卡一卡二卡3卡四卡2021免费 | 国产香蕉九九久久精品免费| 国产成人亚洲日韩欧美| 欧美黑人乱大交| 一本精品99久久精品77| 久久99九九精品久久久久蜜桃 | 男女一级国产片免费视频| 国产午夜福利视频一区二区| 天堂V亚洲国产V第一次| 亚洲精品蜜桃久久久久久| 无码av永久免费专区麻豆| 国产精品不卡一区二区视频| 中文字幕亚洲综合久久2020| 国产一区二区三区av在线无码观看| 亚洲一线二线三线品牌精华液久久久| 亚洲国产欧美一区二区好看电影| 国产无套白浆一区二区| 国产h视频在线观看| 国产三级国产精品国产专| 亚洲高潮喷水无码AV电影| 精品无码国产不卡在线观看| 久久se精品一区二区三区| 国产午夜A理论毛片| 牛牛视频一区二区三区| 亚洲一二区在线视频播放| 免费AV片在线观看网址| 精品少妇人妻av无码久久| 久久伊99综合婷婷久久伊| 亚洲男人精品青春的天堂| 国产老头多毛Gay老年男| 亚欧洲乱码视频在线专区| 视频一区二区三区自拍偷拍| 最近免费中文字幕大全| 国产在线线精品宅男网址| 天堂在线最新版在线天堂| 亚洲aⅴ无码专区在线观看春色| 亚洲欧美人成网站在线观看看|