代码之家  ›  专栏  ›  技术社区  ›  Randolpho

单元测试TransactionScope的使用

  •  27
  • Randolpho  · 技术社区  · 16 年前

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

    问题是: 交易镜 当我期望的时候反对。

    交易镜 以下是一份报告:

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

    问题是: 交易镜 是否根据标准模式适当使用?

    最后的想法: 我已经考虑了一个解决方案,它肯定会提供我所需要的覆盖范围,但我拒绝了它,因为它过于复杂,不符合标准 图案它包括添加一个 CreateTransactionScope 方法,该方法返回 交易镜 . 但由于TransactionScope包含构造函数逻辑和非虚拟方法,因此很难(如果不是不可能)进行模拟, 将返回 DataLayerTransactionScope 这将是一个可模仿的门面 交易镜 .

    虽然这可能会完成这项工作,但它很复杂,我更喜欢使用标准模式。有更好的办法吗?

    5 回复  |  直到 16 年前
        1
  •  28
  •   abatishchev Karl Johan    14 年前

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

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

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

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

    编辑:

    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();
            }
        }
    }
    
        2
  •  5
  •   abatishchev Karl Johan    14 年前

    忽略这个测试是否是件好事。。。。

    非常肮脏的黑客是检查该事务。Current不为null。

    这不是一个100%的测试,因为有人可能使用TransactionScope以外的其他工具来实现这一点,但它应该防止明显的“没有麻烦拥有事务”部分。

    另一种选择是故意尝试创建一个新的TransactionScope,该TransactionScope的隔离级别与将要/应该使用的隔离级别不兼容,并且 TransactionScopeOption.Required

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

        3
  •  4
  •   Mathias Becher    11 年前

    我发现了一个很好的方法来测试这个使用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
  •  3
  •   duffymo    16 年前

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

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

    如果这两个都通过了,我想说你的交易范围正在正常工作。

        5
  •  0
  •   Grubsnik    12 年前

    在我自己思考了同样的问题之后,我得出了以下解决方案。

    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相关的测试。