2012-07-19 68 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