.net 使用TransactionScope进行单元测试

dsf9zpds  于 2023-06-25  发布在  .NET
关注(0)|答案(6)|浏览(122)

**序言:**我设计了一个强接口和完全可模拟的数据层类,当多个调用应该包含在单个事务中时,它期望业务层创建TransactionScope
**问题:**我想对业务层使用TransactionScope对象的情况进行单元测试。

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

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

虽然就程序员的可用性而言,这确实是一个很棒的模式,但它所做的测试似乎……对我来说不可能。我无法检测到一个 transient 对象已经被示例化,更不用说模拟它来确定对它调用了一个方法。然而,我的目标覆盖面意味着我必须。

**问题:**如何构建单元测试,以确保TransactionScope根据标准模式正确使用?
**最后的想法:**我考虑过一个解决方案,它肯定能提供我需要的覆盖率,但我拒绝了它,因为它过于复杂,不符合标准的TransactionScope模式。它涉及到在我的数据层对象上添加一个CreateTransactionScope方法,该方法返回TransactionScope的示例。但是由于TransactionScope包含构造函数逻辑和非虚方法,因此即使不是不可能也很难模拟,CreateTransactionScope将返回DataLayerTransactionScope的示例,这将是TransactionScope的可模拟外观。

虽然这可能会做的工作,它的复杂性,我宁愿使用标准模式。有更好的办法吗?

ut6juiuv

ut6juiuv1#

我现在正面临着同样的问题,对我来说,似乎有两个解决方案:
1.不要解决问题。
1.为现有的类创建抽象,这些类遵循相同的模式,但是是可模拟/可存根的。

**编辑:**我已经为此创建了一个CodePlex-project:http://legendtransactions.codeplex.com/

我倾向于创建一组用于处理事务的接口和一个委托给System的默认实现。

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

3pvhb19x2#

我发现了一个很好的方法来测试这一点,使用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();
    }
}

这对我的目的很有效。

7hiiyaii

7hiiyaii3#

不管这个测试是好是坏...
非常肮脏的黑客是检查交易.当前不是空.
这不是一个100%的测试,因为有人可能使用TransactionScope以外的其他东西来实现这一点,但它应该防止明显的“懒得有事务”部分。
另一种选择是故意尝试创建一个新的TransactionScope,该TransactionScope的隔离级别与任何将/应该使用的和TransactionScopeOption.Required不兼容。如果此操作成功而不是抛出ArgumentException,则不存在事务。这需要您知道某个特定的IsolationLevel未使用(可能会选择Chaos之类的)
这两个选项都不是特别令人满意的,后者非常脆弱,并且受制于TransactionScope的语义保持不变。我会测试前者而不是后者,因为它更健壮(并且易于阅读/调试)。

t9eec4r0

t9eec4r04#

我是一名Java开发人员,所以我不确定C#的细节,但在我看来,你需要两个单元测试。
第一个应该是成功的“蓝天”测试。您的单元测试应该确保所有ACID记录在事务提交后出现在数据库中。
第二个应该是“wonky”版本,它执行InsertFoo操作,然后在尝试InsertBar之前抛出异常。一个成功的测试将显示异常已经被抛出,并且Foo和Bar对象都没有被提交到数据库。
如果这两个都通过了,我会说您的TransactionScope正在正常工作。

byqmnocz

byqmnocz5#

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

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

fhity93d

fhity93d6#

创建一个wrapper:

public interface IDtsManager : IDisposable
{
    void Complete();
}

public class DtsManager :  IDtsManager
{
    private readonly TransactionScope _scope;

    public DtsManager()
    {
        // This is needed dotnet 7+
        TransactionManager.ImplicitDistributedTransactions = true;
        _scope = new TransactionScope();
        TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
    }

    public void Complete()
    {
        _scope.Complete();
    }

    public void Dispose()
    {
        _scope.Dispose();
    }
}

然后用工厂方法进行DI

public class MyService
{
    private readonly Func<IDtsManager> _dtsManagerFactory;
    
    public MyService(Func<IDtsManager> dtsManagerFactory)
    {
        _dtsManagerFactory = dtsManagerFactory;
    }
    
    public void MyMethod()
    {
        using (var scope = _dtsManagerFactory())
        {
            // your code
            scope.Complete();
        }
    }
}

相关问题