unit testingTransactionScope的使用

序言:我设计了一个强接口和完全可模拟的数据层类,当多个调用应该包含在单个TransactionScope时,期望业务层创建一个TransactionScope

问题:我希望unit testing我的业务层在我期望的时候使用TransactionScope对象。

不幸的是,使用TransactionScope的标准模式如下:

 using(var scope = new TransactionScope()) { // transactional methods datalayer.InsertFoo(); datalayer.InsertBar(); scope.Complete(); } 

虽然这对于程序员的可用性而言是一个非常好的模式,但测试它已经完成似乎……对我来说是不可能的。 我无法检测到已经实例化了一个临时对象,更不用说模拟它以确定在其上调用了一个方法。 然而,我的报道目标意味着我必须这样做。

问题:如何构建unit testing以确保根据标准模式正确使用TransactionScope

最后的想法:我已经考虑过一个肯定会提供我需要的覆盖范围的解决方案,但是因为过于复杂而且不符合标准的TransactionScope模式而拒绝了它。 它涉及在我的数据层对象上添加CreateTransactionScope方法,该方法返回TransactionScope一个实例。 但是因为TransactionScope包含构造函数逻辑和非虚拟方法,因此很难(如果不是不可能)进行模拟, CreateTransactionScope将返回一个DataLayerTransactionScope实例, DataLayerTransactionScope成为TransactionScope可模拟外观。

虽然这可能做得很复杂,但我更喜欢使用标准模式。 有没有更好的办法?

我现在正坐在同样的问题上,对我来说似乎有两种解决方案:

  1. 不解决问题。
  2. 为遵循相同模式但是可模拟/可存根的现有类创建抽象。

编辑:我现在已经为此创建了一个CodePlex项目: http : //legendtransactions.codeplex.com/

我倾向于创建一组用于处理事务的接口和一个委托给System.Transaction实现的默认实现,例如:

 public interface ITransactionManager { ITransaction CurrentTransaction { get; } ITransactionScope CreateScope(TransactionScopeOption options); } public interface ITransactionScope : IDisposable { void Complete(); } public interface ITransaction { void EnlistVolatile(IEnlistmentNotification enlistmentNotification); } public interface IEnlistment { void Done(); } public interface IPreparingEnlistment { void Prepared(); } public interface IEnlistable // The same as IEnlistmentNotification but it has // to be redefined since the Enlistment-class // has no public constructor so it's not mockable. { void Commit(IEnlistment enlistment); void Rollback(IEnlistment enlistment); void Prepare(IPreparingEnlistment enlistment); void InDoubt(IEnlistment enlistment); } 

这似乎是很多工作,但另一方面它是可重复使用的,它使得它很容易测试。

请注意,这不是接口的完整定义,足以让您了解全局。

编辑:我只是做了一些快速和肮脏的实现作为概念的certificate,我认为这是我将采取的方向,这是我到目前为止所提出的。 我想也许我应该为此创建一个CodePlex项目,以便一劳永逸地解决问题。 这不是我第一次碰到这个。

 public interface ITransactionManager { ITransaction CurrentTransaction { get; } ITransactionScope CreateScope(TransactionScopeOption options); } public class TransactionManager : ITransactionManager { public ITransaction CurrentTransaction { get { return new DefaultTransaction(Transaction.Current); } } public ITransactionScope CreateScope(TransactionScopeOption options) { return new DefaultTransactionScope(new TransactionScope()); } } public interface ITransactionScope : IDisposable { void Complete(); } public class DefaultTransactionScope : ITransactionScope { private TransactionScope scope; public DefaultTransactionScope(TransactionScope scope) { this.scope = scope; } public void Complete() { this.scope.Complete(); } public void Dispose() { this.scope.Dispose(); } } public interface ITransaction { void EnlistVolatile(Enlistable enlistmentNotification, EnlistmentOptions enlistmentOptions); } public class DefaultTransaction : ITransaction { private Transaction transaction; public DefaultTransaction(Transaction transaction) { this.transaction = transaction; } public void EnlistVolatile(Enlistable enlistmentNotification, EnlistmentOptions enlistmentOptions) { this.transaction.EnlistVolatile(enlistmentNotification, enlistmentOptions); } } public interface IEnlistment { void Done(); } public interface IPreparingEnlistment { void Prepared(); } public abstract class Enlistable : IEnlistmentNotification { public abstract void Commit(IEnlistment enlistment); public abstract void Rollback(IEnlistment enlistment); public abstract void Prepare(IPreparingEnlistment enlistment); public abstract void InDoubt(IEnlistment enlistment); void IEnlistmentNotification.Commit(Enlistment enlistment) { this.Commit(new DefaultEnlistment(enlistment)); } void IEnlistmentNotification.InDoubt(Enlistment enlistment) { this.InDoubt(new DefaultEnlistment(enlistment)); } void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment) { this.Prepare(new DefaultPreparingEnlistment(preparingEnlistment)); } void IEnlistmentNotification.Rollback(Enlistment enlistment) { this.Rollback(new DefaultEnlistment(enlistment)); } private class DefaultEnlistment : IEnlistment { private Enlistment enlistment; public DefaultEnlistment(Enlistment enlistment) { this.enlistment = enlistment; } public void Done() { this.enlistment.Done(); } } private class DefaultPreparingEnlistment : DefaultEnlistment, IPreparingEnlistment { private PreparingEnlistment enlistment; public DefaultPreparingEnlistment(PreparingEnlistment enlistment) : base(enlistment) { this.enlistment = enlistment; } public void Prepared() { this.enlistment.Prepared(); } } } 

这是一个依赖ITransactionManager处理事务工作的类的示例:

 public class Foo { private ITransactionManager transactionManager; public Foo(ITransactionManager transactionManager) { this.transactionManager = transactionManager; } public void DoSomethingTransactional() { var command = new TransactionalCommand(); using (var scope = this.transactionManager.CreateScope(TransactionScopeOption.Required)) { this.transactionManager.CurrentTransaction.EnlistVolatile(command, EnlistmentOptions.None); command.Execute(); scope.Complete(); } } private class TransactionalCommand : Enlistable { public void Execute() { // Do some work here... } public override void Commit(IEnlistment enlistment) { enlistment.Done(); } public override void Rollback(IEnlistment enlistment) { // Do rollback work... enlistment.Done(); } public override void Prepare(IPreparingEnlistment enlistment) { enlistment.Prepared(); } public override void InDoubt(IEnlistment enlistment) { enlistment.Done(); } } } 

忽略这个测试是否是好事….

非常脏的黑客是检查Transaction.Current是否为空。

这不是100%的测试,因为有人可能会使用除了TransactionScope之外的其他东西来实现这一点,但它应该防止显而易见的“没有费心去交易”的部分。

另一种选择是故意尝试创建一个新的TransactionScope,它具有与任何将要/应该使用的不兼容的隔离级别和TransactionScopeOption.Required 。 如果这成功而不是抛出ArgumentException,那么就没有事务。 这要求您知道特定的IsolationLevel未使用(像混沌这样的东西是一个潜在的选择)

这两个选项都不是特别令人愉快,后者非常脆弱,并且受到TransactionScope保持不变的语义的影响。 我会测试前者而不是后者,因为它更健壮(并且读取/调试清晰)。

我是一名Java开发人员,所以我不确定C#的详细信息,但在我看来,你需要在这里进行两次unit testing。

第一个应该是成功的“蓝天”测试。 您的unit testing应确保在提交事务后所有ACID记录都出现在数据库中。

第二个应该是“wonky”版本,它执行InsertFoo操作,然后在尝试InsertBar之前抛出exception。 成功的测试将显示抛出exception并且Foo和Bar对象都未提交到数据库。

如果这两个都通过,我会说你的TransactionScope正在按预期工作。

我发现了一种使用Moq和FluentAssertions进行测试的好方法。 假设您的待测单位如下所示:

 public class Foo { private readonly IDataLayer dataLayer; public Foo(IDataLayer dataLayer) { this.dataLayer = dataLayer; } public void MethodToTest() { using (var transaction = new TransactionScope()) { this.dataLayer.Foo(); this.dataLayer.Bar(); transaction.Complete(); } } } 

您的测试看起来像这样(假设MS测试):

 [TestClass] public class WhenMethodToTestIsCalled() { [TestMethod] public void ThenEverythingIsExecutedInATransaction() { var transactionCommitted = false; var fooTransaction = (Transaction)null; var barTransaction = (Transaction)null; var dataLayerMock = new Mock(); dataLayerMock.Setup(dataLayer => dataLayer.Foo()) .Callback(() => { fooTransaction = Transaction.Current; fooTransaction.TransactionCompleted += (sender, args) => transactionCommitted = args.Transaction.TransactionInformation.Status == TransactionStatus.Committed; }); dataLayerMock.Setup(dataLayer => dataLayer.Bar()) .Callback(() => barTransaction = Transaction.Current); var unitUnderTest = new Foo(dataLayerMock.Object); unitUnderTest.MethodToTest(); // A transaction was used for Foo() fooTransaction.Should().NotBeNull(); // The same transaction was used for Bar() barTransaction.Should().BeSameAs(fooTransaction); // The transaction was committed transactionCommitted.Should().BeTrue(); } } 

这对我的目的很有用。

在我自己考虑了同样的问题后,我找到了以下解决方案。

将模式更改为:

 using(var scope = GetTransactionScope()) { // transactional methods datalayer.InsertFoo(); datalayer.InsertBar(); scope.Complete(); } protected virtual TransactionScope GetTransactionScope() { return new TransactionScope(); } 

然后,当您需要测试代码时,inheritance被测试的类,扩展该函数,以便检测它是否被调用。

 public class TestableBLLClass : BLLClass { public bool scopeCalled; protected override TransactionScope GetTransactionScope() { this.scopeCalled = true; return base.GetTransactionScope(); } } 

然后,在类的可测试版本上执行与TransactionScope相关的测试。