繁体   English   中英

对 TransactionScope 的使用进行单元测试

[英]Unit Testing the Use of TransactionScope

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

问题:我想对我的业务层使用TransactionScope object 进行单元测试。

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

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

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

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

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

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

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

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

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

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

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

下面是一个 class 的示例,它依赖于 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 不是 null。

这不是一个 100% 的测试,因为有人可能会使用 TransactionScope 以外的东西来实现这一点,但它应该防止明显的“没有费心进行事务”部分。

另一种选择是故意尝试创建一个新的 TransactionScope,其隔离级别与将/应该使用的任何内容和TransactionScopeOption.Required不兼容。 如果这成功而不是抛出 ArgumentException 则没有事务。 这需要你知道一个特定的 IsolationLevel 是未使用的(像 Chaos 之类的东西是一个潜在的选择)

这两个选项都不是特别令人愉快,后者非常脆弱并且受 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<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();
    }
}

这对我的目的很有用。

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

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

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

如果这两个都通过,我会说您的 TransactionScope 正在正常工作。

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

将模式更改为:

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

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

然后,当您需要测试您的代码时,您继承被测 Class,扩展 function,因此您可以检测它是否被调用。

public class TestableBLLClass : BLLClass
    {
        public bool scopeCalled;

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM