单元测试TransactionScope的使用

时间:2009-03-09 15:50:44

标签: c# .net unit-testing transactions

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

问题:我想对我的业务层在我预期的时候使用TransactionScope对象进行单元测试。

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

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

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

问题:如何构建单元测试以确保根据标准模式正确使用TransactionScope

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

虽然这可能会完成这项工作,但它更复杂,我更愿意使用标准模式。还有更好的方法吗?

5 个答案:

答案 0 :(得分:28)

我刚刚遇到同样的问题而且对我来说似乎有两种解决方案:

  1. 不解决问题。
  2. 为遵循相同模式但可模拟/可存根的现有类创建抽象。
  3. 修改 我现在为此创建了一个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);
    
    }
    

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

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

    修改 我只是做了一些快速而肮脏的实现作为概念的证明,我认为这是我将采取的方向,这是我到目前为止所提出的。我想也许我应该为此创建一个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();
            }
        }
    }
    

答案 1 :(得分:5)

忽略此测试是否是好事....

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

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

另一种选择是故意尝试创建一个新的TransactionScope,其隔离级别与使用和/ TransactionScopeOption.Required中的任何内容不相容。如果这成功而不是抛出ArgumentException,则没有事务。这要求您知道特定的IsolationLevel未使用(类似混沌是一种潜在的选择)

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

答案 2 :(得分:3)

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

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

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

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

答案 3 :(得分:3)

我找到了一种使用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<IDataLayer>();

        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();
    }
}

这对我的目的很有用。

答案 4 :(得分:0)

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

将模式更改为:

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

protected virtual TransactionScope GetTransactionScope()
{
    return new TransactionScope();
}

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

public class TestableBLLClass : BLLClass
    {
        public bool scopeCalled;

        protected override TransactionScope GetTransactionScope()
        {
            this.scopeCalled = true;
            return base.GetTransactionScope();
        }
    }

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