我们没有使用TDD,所以单元测试麻烦的是准备测试的基础数据。我们现在是使用内存仓储来做单元测试,要为每个仓储都构造基础数据,非常麻烦。
前几天看xunit的源码,看到AutoRollbackAttribute这个特性,异常的兴奋 ^_^。怎么忘了用事务的自动回滚呢?
我们看AutorollbackAttribute的具体实现:
public class AutoRollbackAttribute : BeforeAfterTestAttribute { IsolationLevel isolationLevel = IsolationLevel.Unspecified; TransactionScope scope; TransactionScopeOption scopeOption = TransactionScopeOption.Required; long timeoutInMS = -1;
/// <summary> /// Gets or sets the isolation level of the transaction. /// Default value is <see cref="IsolationLevel"/>.Unspecified. /// </summary> public IsolationLevel IsolationLevel { get { return isolationLevel; } set { isolationLevel = value; } }
/// <summary> /// Gets or sets the scope option for the transaction. /// Default value is <see cref="TransactionScopeOption"/>.Required. /// </summary> public TransactionScopeOption ScopeOption { get { return scopeOption; } set { scopeOption = value; } }
/// <summary> /// Gets or sets the timeout of the transaction, in milliseconds. /// By default, the transaction will not timeout. /// </summary> public long TimeoutInMS { get { return timeoutInMS; } set { timeoutInMS = value; } }
/// <summary> /// Rolls back the transaction. /// </summary> public override void After(MethodInfo methodUnderTest) { scope.Dispose(); }
/// <summary> /// Creates the transaction. /// </summary> public override void Before(MethodInfo methodUnderTest) { TransactionOptions options = new TransactionOptions(); options.IsolationLevel = isolationLevel; if (timeoutInMS > 0) options.Timeout = new TimeSpan(timeoutInMS * 10); scope = new TransactionScope(scopeOption, options); } }
这里使用了.Net Framework自带的TransactionScope。TransactionScope在.NET 2.0中已经有了,可用于分布式事务。用这种方法来做数据的自动回滚也有一些不足:
1、数据库要支持事务。
2、内部数据库操作的逻辑里没有事务的实现。
很庆幸的是我们的项目正好都满足上面的2点,不足的是mongodb不支持事务。所以需要混合仓储实现了,事务数据库使用真实的仓储,mongodb使用内存仓储。
项目中是用VS自带的单元测试框架,也不想因为这一个特性而改用xunit,那只能动手把这个迁移到VS的单元测试框架里了。
/// <summary> /// 单元测试基类 /// </summary> [TestClass] public class BaseUnitTest { IsolationLevel _isolationLevel = IsolationLevel.Unspecified; TransactionScopeOption _scopeOption = TransactionScopeOption.Required; TransactionScope _transactionScope; bool _openAutoRollback = true;
/// <summary> /// 构造函数 /// </summary> /// <param name="autoRollback">是否开启自动回滚,默认开启</param> public BaseUnitTest(bool autoRollback = true) { _openAutoRollback = autoRollback; }
/// <summary> /// 自动回滚事务初始化 /// </summary> [TestInitialize] public void AutoRollbackBefore() { if (_openAutoRollback) { var options = new TransactionOptions(); options.IsolationLevel = _isolationLevel; options.Timeout = new TimeSpan(0, 1, 0); _transactionScope = new TransactionScope(_scopeOption, options); } }
/// <summary> /// 自动回滚事务回滚并释放对象 /// </summary> [TestCleanup] public void AutoRollbackAfter() { if (_openAutoRollback) { if (_transactionScope == null) throw new InvalidOperationException("未初始化TransactionScope"); //回滚事务 _transactionScope.Dispose(); //释放事务对象 _transactionScope = null; //移除所有的缓存 RemoveHttpRuntimeCache(); } }
/// <summary> /// 移除所有的HttpRuntime缓存 /// </summary> [DebuggerStepThrough] private void RemoveHttpRuntimeCache() { var cache = HttpRuntime.Cache.GetEnumerator(); var keys = new List<string>(); while (cache.MoveNext()) { keys.Add(cache.Key.ToString()); } foreach (var key in keys) { HttpRuntime.Cache.Remove(key); } }
/// <summary> /// 设置不自动回滚事务 /// </summary> protected void SetAutoRollbackIsUnavailabled() { _openAutoRollback = false; } }
上面的RemoveHttpRuntimeCache是因为我们在项目中有使用HttpRuntime缓存,关系数据库中的数据回滚后会导致缓存和数据库不一致,所以一但有开启事务的自动回滚,也要相应的清空内存缓存。
方法很简单,跟大家分享一下,和TransactionScope相关的知识,不清楚的同学可以看下MSDN关于“TransactionScope”的文档。