2012-07-19 50 views
1

編輯:我已經決定徹底​​拋棄這個想法,因爲雖然很高興使用相同的方法來創建我的實例,但我放棄了其他事情並使問題複雜化。單元測試可維護性和工廠

在我的單元測試項目中,我有一個基本上包含我的單元測試的所有工廠類的文件夾,例如下面的文件夾。

public static class PackageFileInfoFactory 
{ 
    private const string FILE_NAME = "testfile.temp"; 

    public static PackageFileInfo CreateFullTrustInstance() 
    { 
     var mockedFileInfo = CreateMockedInstance(AspNetHostingPermissionLevel.Unrestricted); 

     return mockedFileInfo.Object; 
    } 

    public static PackageFileInfo CreateMediumTrustInstance() 
    { 
     var mockedFileInfo = CreateMockedInstance(AspNetHostingPermissionLevel.Medium); 

     return mockedFileInfo.Object; 
    } 

    private static Mock<PackageFileInfo> CreateMockedInstance(AspNetHostingPermissionLevel trustLevel) 
    { 
     var mockedFileInfo = new Mock<PackageFileInfo>(FILE_NAME); 

     mockedFileInfo.Protected().SetupGet<AspNetHostingPermissionLevel>("TrustLevel").Returns(() => trustLevel); 

     mockedFileInfo.Protected().Setup<string>("CopyTo", ItExpr.IsAny<string>()).Returns<string>(destFileName => "Some Unknown Path"); 

     return mockedFileInfo; 
    } 
} 

這是我的單元測試的一個示例。

public class PackageFileInfoTest 
{ 
    public class Copy 
    { 
     [Fact] 
     public void Should_throw_exception_in_medium_trust_when_probing_directory_does_not_exist() 
     { 
      // Arrange 
      var fileInfo = PackageFileInfoFactory.CreateMediumTrustInstance(); 

      fileInfo.ProbingDirectory = "SomeDirectory"; 

      // Act 
      Action act =() => fileInfo.Copy(); 

      // Assert 
      act.ShouldThrow<InvalidOperationException>(); 
     } 

     [Fact] 
     public void Should_throw_exception_in_full_trust_when_probing_directory_does_not_exist() 
     { 
      // Arrange 
      var fileInfo = PackageFileInfoFactory.CreateFullTrustInstance(); 

      fileInfo.ProbingDirectory = "SomeDirectory"; 

      // Act 
      Action act =() => fileInfo.Copy(); 

      // Assert 
      act.ShouldThrow<InvalidOperationException>(); 
     } 

     [Fact] 
     public void Should_throw_exception_when_probing_directory_is_null_or_empty() 
     { 
      // Arrange 
      var fileInfo = PackageFileInfoFactory.CreateFullTrustInstance(); 

      // Act 
      Action act =() => fileInfo.Copy(); 

      // Assert 
      act.ShouldThrow<InvalidOperationException>(); 
     } 
    } 
} 

這有助於我保持我的單元測試乾淨和重點測試,我只是想知道其他人如何處理這一點,他們在做什麼,以保持清潔測試。

UPDATE:

針對Adronius我已經更新了我的帖子有一個原型是我的目標在減少這些工廠。

一個主要問題是在我的測試中使用完全相同的語法來創建實例並減少工廠類的數量。

namespace EasyFront.Framework.Factories 
{ 
    using System; 
    using System.Collections.Generic; 
    using System.Diagnostics.Contracts; 

    using EasyFront.Framework.Diagnostics.Contracts; 

    /// <summary> 
    ///  Provides a container to objects that enable you to setup them and resolve instances by type. 
    /// </summary> 
    /// <remarks> 
    ///  Eyal Shilony, 20/07/2012. 
    /// </remarks> 
    public class ObjectContainer 
    { 
     private readonly Dictionary<string, object> _registeredTypes; 

     public ObjectContainer() 
     { 
      _registeredTypes = new Dictionary<string, object>(); 
     } 

     public TResult Resolve<TResult>() 
     { 
      string keyAsString = typeof(TResult).FullName; 

      return Resolve<TResult>(keyAsString); 
     } 

     public void AddDelegate<TResult>(Func<TResult> func) 
     { 
      Contract.Requires(func != null); 

      Add(typeof(TResult).FullName, func); 
     } 

     protected virtual TResult Resolve<TResult>(string key) 
     { 
      Contract.Requires(!string.IsNullOrEmpty(key)); 

      if (ContainsKey(key)) 
      { 
       Func<TResult> func = GetValue<Func<TResult>>(key); 

       Assert.NotNull(func); 

       return func(); 
      } 

      ThrowWheNotFound<TResult>(); 

      return default(TResult); 
     } 

     protected void Add<T>(string key, T value) where T : class 
     { 
      Contract.Requires(!string.IsNullOrEmpty(key)); 

      _registeredTypes.Add(key, value); 
     } 

     protected bool ContainsKey(string key) 
     { 
      Contract.Requires(!string.IsNullOrEmpty(key)); 

      return _registeredTypes.ContainsKey(key); 
     } 

     protected T GetValue<T>(string key) where T : class 
     { 
      Contract.Requires(!string.IsNullOrEmpty(key)); 

      return _registeredTypes[key] as T; 
     } 

     protected void ThrowWheNotFound<TResult>() 
     { 
      throw new InvalidOperationException(string.Format("The type '{0}' was not found in type '{1}'.", typeof(TResult).FullName, GetType().ReflectedType.FullName)); 
     } 

     [ContractInvariantMethod] 
     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")] 
     private void ObjectInvariant() 
     { 
      Contract.Invariant(_registeredTypes != null); 
     } 

    } 
} 

然後,我可以擴展這個採取這樣的參數。

namespace EasyFront.Framework.Factories 
{ 
    using System; 
    using System.Collections.Generic; 
    using System.Diagnostics.Contracts; 

    using EasyFront.Framework.Diagnostics.Contracts; 

    public class ObjectContainer<T> : ObjectContainer 
    { 
     public TResult Resolve<TResult>(T key = default(T)) 
     { 
      string keyAsString = EqualityComparer<T>.Default.Equals(key, default(T)) ? typeof(TResult).FullName : GetKey(key); 

      Assert.NotNullOrEmpty(keyAsString); 

      return Resolve<TResult>(keyAsString); 
     } 

     public void AddDelegate<TReturn>(T key, Func<T, TReturn> func) 
     { 
      Contract.Requires(func != null); 

      Add(GetKey(key), func); 
     } 

     protected override TResult Resolve<TResult>(string key) 
     { 
      if (ContainsKey(key)) 
      { 
       Func<TResult> func = GetValue<Func<TResult>>(key); 

       Assert.NotNull(func); 

       return func(); 
      } 

      throw new InvalidOperationException(string.Format("The type '{0}' was not setup for type '{1}'.", typeof(TResult).FullName, GetType().ReflectedType.FullName)); 
     } 

     /// <summary> Gets the full name of the type and the hash code as the key. </summary> 
     /// <remarks> Eyal Shilony, 20/07/2012. </remarks> 
     /// <param name="value"> The value to use to get key. </param> 
     /// <returns> The full name of the type and the hash code as the key. </returns> 
     private static string GetKey(T value) 
     { 
      Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); 

      string key = value.GetType().FullName + "#" + value.ToString().GetHashCode(); 

      Assert.NotNullOrEmpty(key); 

      return key; 
     } 
    } 
} 

執行將是這樣的。

namespace EasyFront.Tests.Factories 
{ 
    using System.Web; 

    using EasyFront.Framework.Factories; 
    using EasyFront.Framework.Web.Hosting.Packages; 

    using Moq; 
    using Moq.Protected; 

    public class PackageFileInfoFactory : IObjectFactory<AspNetHostingPermissionLevel> 
    { 
     private const string FILE_NAME = "testfile.temp"; 

     private readonly ObjectContainer<AspNetHostingPermissionLevel> _container; 

     public PackageFileInfoFactory() 
     { 
      _container = new ObjectContainer<AspNetHostingPermissionLevel>(); 

      _container.AddDelegate(AspNetHostingPermissionLevel.Unrestricted, value => 
      { 
       var mockedFileInfo = CreateMockedInstance(value); 

       return mockedFileInfo.Object; 
      }); 

      _container.AddDelegate(AspNetHostingPermissionLevel.Medium, value => 
      { 
       var mockedFileInfo = CreateMockedInstance(value); 

       return mockedFileInfo.Object; 
      }); 
     } 

     public TResult CreateInstance<TResult>(AspNetHostingPermissionLevel first) 
     { 
      return _container.Resolve<TResult>(first); 
     } 

     private static Mock<PackageFileInfo> CreateMockedInstance(AspNetHostingPermissionLevel trustLevel) 
     { 
      var mockedFileInfo = new Mock<PackageFileInfo>(FILE_NAME); 

      mockedFileInfo.Protected().SetupGet<AspNetHostingPermissionLevel>("TrustLevel").Returns(() => trustLevel); 

      mockedFileInfo.Protected().Setup<string>("CopyTo", ItExpr.IsAny<string>()).Returns<string>(destFileName => "Some Unknown Path"); 

      return mockedFileInfo; 
     } 
    } 
} 

最後我可以像這樣使用它。

namespace EasyFront.Framework.Web.Hosting.Packages 
{ 
    using System; 
    using System.Web; 

    using EasyFront.Tests.Factories; 

    using FluentAssertions; 

    using global::Xunit; 

    public class PackageFileInfoTest 
    { 
     public class Copy 
     { 
      private readonly PackageFileInfoFactory _factory; 

      public Copy() 
      { 
       _factory = new PackageFileInfoFactory(); 
      } 

      [Fact] 
      public void Should_throw_exception_in_medium_trust_when_probing_directory_does_not_exist() 
      { 
       // Arrange 
       var fileInfo = _factory.CreateInstance<PackageFileInfo>(AspNetHostingPermissionLevel.Medium); 

       fileInfo.ProbingDirectory = "SomeDirectory"; 

       // Act 
       Action act =() => fileInfo.Copy(); 

       // Assert 
       act.ShouldThrow<InvalidOperationException>(); 
      } 

      [Fact] 
      public void Should_throw_exception_in_full_trust_when_probing_directory_does_not_exist() 
      { 
       // Arrange 
       var fileInfo = _factory.CreateInstance<PackageFileInfo>(AspNetHostingPermissionLevel.Unrestricted); 

       fileInfo.ProbingDirectory = "SomeDirectory"; 

       // Act 
       Action act =() => fileInfo.Copy(); 

       // Assert 
       act.ShouldThrow<InvalidOperationException>(); 
      } 

      [Fact] 
      public void Should_throw_exception_when_probing_directory_is_null_or_empty() 
      { 
       // Arrange 
       var fileInfo = _factory.CreateInstance<PackageFileInfo>(AspNetHostingPermissionLevel.Unrestricted); 

       // Act 
       Action act =() => fileInfo.Copy(); 

       // Assert 
       act.ShouldThrow<InvalidOperationException>(); 
      } 
     } 
    } 
} 

我不知道它的工作或沒有,它只是我的職位,以證明我的觀點做了一個概念,我不知道別人怎麼想呢?如果您有任何建議或任何我會很樂意聽到它。

我不喜歡重新發明輪子,所以如果你有更好的方法,我也想聽聽。 :)

回答

0

我在2分鐘內無法理解更新中的test-factory-class-framework,所以我認爲它不會讓維護測試變得更容易。

但是我喜歡你有一個集中的testdatagenerator的概念。

在我看來,每種類型的測試數據工廠方法在大多數情況下應該足夠了。該方法定義了該類型的標準Testdata。

實際測試將差異分配給標準測試數據。

簡單examle:

public class PackageFileInfoTest 
{ 
    public class Copy 
    { 
     [Fact] 
     public void Should_throw_exception_when_probing_directory_does_not_exist() 
     { 
      // Arrange 
      var fileInfo = TestData.CreatePackageFileInfo(); 
      fileInfo.ProbingDirectory = "/Some/Directory/That/Does/Not/Exist"; 
      // Act 
      Action act =() => fileInfo.Copy(); 

      // Assert 
      act.ShouldThrow<InvalidOperationException>(); 
     } 

注: 「非現有目錄測試」 應爲 「權限級別」 的獨立的。因此,工廠的標準許可應該包括在內。

每一個類型的factorymethod更復雜examle當不止一種類型的參與:

 [Fact] 
     public void Should_throw_exception_in_low_trust_when_writing_to_protected_directory() 
     { 
      // Arrange 
      var protectedDirectory = TestData.CreateDirectory(); 
      protectedDirectory.MinimalPermissionRequired = AspNetHostingPermissionLevel.Full; 

      var currentUser= TestData.CreateUser(); 
      currentUser.TrustLevel = AspNetHostingPermissionLevel.Low; 

      // Act 
      Action act =() => FileUploadService.CopyTo(protectedDirectory); 

      .... 
    } 
} 

在DOTNET世界nbuilder可以幫助你來填充TESTDATA的陣列。

+0

不知道NBuilder,這似乎很有用,雖然我不認爲它給了我相同的功能,但我可能是錯的。 你能告訴我你在那裏不理解嗎?該類的用法或實現? 實際的複製方法完全獨立於文件系統,我必須提供信任級別的原因是因爲PackageFileInfo對於每個信任級別的行爲不同。 – 2012-07-21 07:26:03

+0

非常感謝NBuilder,儘管它對我來說可能是非常有用的,如果我可以投票的話,我只是爲了這個。 :) – 2012-07-21 07:33:25

+0

我使用拇指規則說:「如果我需要超過2分鐘才能獲得某些實用方法/類的好處,處理和/或含義,那麼它可能太複雜(codesmell)。」 我只是沒有投入超過這2分鐘,以找出「旨在減少這些工廠」與代碼一起的意思。這並不意味着你的解決方案根本不好。瞭解DependencyInjection和InversionOfControl花了我幾個小時,我喜歡這些.... – k3b 2012-07-21 08:51:10

0

我在單元測試中使用了幾乎相同的方法。

如果我在一個測試夾具類中有複製性,大部分創建(不是初始化)的mock(雙打)我放入setUp方法。

但是在幾個測試夾具類中仍然存在重複。因此,對於那些複製性,我使用「TestDoubleFactory」類似於您的靜態類,除非我沒有創建實例的方法,但我總是隻創建模擬對象,因此我可以在測試中進一步修改(設置)它們。

+0

我試圖刪除這些工廠我的主要問題之一是我沒有獲得相同的語法來解決實例,所以我想以相同的方式創建實例。 我想過使用IoC,但我真的不需要大錘來破解堅果,所以我想出了一個使用包含委託的輕量級容器的想法,我會用我的原型更新我的文章。 – 2012-07-21 02:31:59