編輯:我已經決定徹底拋棄這個想法,因爲雖然很高興使用相同的方法來創建我的實例,但我放棄了其他事情並使問題複雜化。單元測試可維護性和工廠
在我的單元測試項目中,我有一個基本上包含我的單元測試的所有工廠類的文件夾,例如下面的文件夾。
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>();
}
}
}
}
我不知道它的工作或沒有,它只是我的職位,以證明我的觀點做了一個概念,我不知道別人怎麼想呢?如果您有任何建議或任何我會很樂意聽到它。
我不喜歡重新發明輪子,所以如果你有更好的方法,我也想聽聽。 :)
不知道NBuilder,這似乎很有用,雖然我不認爲它給了我相同的功能,但我可能是錯的。 你能告訴我你在那裏不理解嗎?該類的用法或實現? 實際的複製方法完全獨立於文件系統,我必須提供信任級別的原因是因爲PackageFileInfo對於每個信任級別的行爲不同。 – 2012-07-21 07:26:03
非常感謝NBuilder,儘管它對我來說可能是非常有用的,如果我可以投票的話,我只是爲了這個。 :) – 2012-07-21 07:33:25
我使用拇指規則說:「如果我需要超過2分鐘才能獲得某些實用方法/類的好處,處理和/或含義,那麼它可能太複雜(codesmell)。」 我只是沒有投入超過這2分鐘,以找出「旨在減少這些工廠」與代碼一起的意思。這並不意味着你的解決方案根本不好。瞭解DependencyInjection和InversionOfControl花了我幾個小時,我喜歡這些.... – k3b 2012-07-21 08:51:10