C# 单元测试 TransactionScope 的使用

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/626802/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 10:50:55  来源:igfitidea点击:

Unit Testing the Use of TransactionScope

c#.netunit-testingtransactions

提问by Randolpho

The preamble:I have designed a strongly interfaced and fully mockable data layer class that expects the business layer to create a TransactionScopewhen multiple calls should be included in a single transaction.

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

The problem:I would like to unit test that my business layer makes use of a TransactionScopeobject when I expect it to.

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

Unfortunately, the standard pattern for using TransactionScopeis a follows:

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

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

While this is a really great pattern in terms of usability for the programmer, testing that it's done seems... unpossible to me. I cannot detect that a transient object has been instantiated, let alone mock it to determine that a method was called on it. Yet my goal for coverage implies that I must.

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

The Question:How can I go about building unit tests that ensure TransactionScopeis used appropriately according to the standard pattern?

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

Final Thoughts:I've considered a solution that would certainly provide the coverage I need, but have rejected it as overly complex and not conforming to the standard TransactionScopepattern. It involves adding a CreateTransactionScopemethod on my data layer object that returns an instance of TransactionScope. But because TransactionScope contains constructor logic and non-virtual methods and is therefore difficult if not impossible to mock, CreateTransactionScopewould return an instance of DataLayerTransactionScopewhich would be a mockable facade into TransactionScope.

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

While this might do the job it's complex and I would prefer to use the standard pattern. Is there a better way?

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

采纳答案by Patrik H?gne

I'm just now sitting with the same problem and to me there seems to be two solutions:

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

  1. Don't solve the problem.
  2. Create abstractions for the existing classes that follows the same pattern but are mockable/stubable.
  1. 不要解决问题。
  2. 为遵循相同模式但可模拟/可存根的现有类创建抽象。

Edit:I've created a CodePlex-project for this now: http://legendtransactions.codeplex.com/

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

I'm leaning towards creating a set of interfaces for working with transactions and a default implementation that delegates to the System.Transaction-implementations, something like:

我倾向于创建一组用于处理事务的接口和一个委托给 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);

}

This seems like a lot of work but on the other hand it's reusable and it makes it all very easily testable.

这看起来需要做很多工作,但另一方面,它是可重用的,而且很容易测试。

Note that this is not the complete definition of the interfaces just enough to give you the big picture.

请注意,这不是接口的完整定义,不足以为您提供大图。

Edit:I just did some quick and dirty implementation as a proof of concept, I think this is the direction I will take, here's what I've come up with so far. I'm thinking that maybe I should create a CodePlex project for this so the problem can be solved once and for all. This is not the first time I've run into this.

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

Here's an example of a class that depends on the ITransactionManager to handle it's transactional work:

下面是一个依赖于 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();
        }
    }
}

回答by duffymo

I'm a Java developer, so I'm uncertain about the C# details, but it seems to me that you need two unit tests here.

我是一名 Java 开发人员,所以我不确定 C# 的细节,但在我看来,这里需要两个单元测试。

The first one should be a "blue sky" test that succeeds. Your unit test should ensure that all records that are ACID appear in the database after the transaction is committed.

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

The second one should be "wonky" version that does the InsertFoo operation and then throws an exception before attempting the InsertBar. A successful test will show that the exception has been thrown and that neither the Foo nor Bar objects have been committed to the database.

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

If both of these pass, I'd say that your TransactionScope is working as it should.

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

回答by ShuggyCoUk

Ignoring whether this test is a good thing or not....

忽略这个测试是不是好事....

Very dirty hack is to check that Transaction.Current is not null.

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

This is not a 100% test since someone could be using something other than TransactionScope to achieve this but it should guard against the obvious 'didn't bother to have a transaction' parts.

这不是 100% 的测试,因为有人可能会使用 TransactionScope 以外的其他东西来实现这一点,但它应该防止明显的“不想进行交易”部分。

Another option is to deliberately try to create a new TransactionScope with incompatible isolation level to whatever would/should be in use and TransactionScopeOption.Required. If this succeeds rather than throwing an ArgumentException there wasn't a transaction. This requires you to know that a particular IsolationLevel is unused (something like Chaos is a potential choice)

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

Neither of these two options is particularly pleasant, the latter is very fragile and subject to the semantics of TransactionScope remaining constant. I would test the former rather than the latter since it is somewhat more robust (and clear to read/debug).

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

回答by Grubsnik

After having thought through the same issue myself, I came to the following solution.

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

Change the pattern to:

将模式更改为:

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

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

When you then need to test your code, you inherit the Class under test, extending the function, so you can detect if it was invoked.

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

public class TestableBLLClass : BLLClass
    {
        public bool scopeCalled;

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

You then perform the tests relating to TransactionScope on the testable version of your class.

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

回答by Mathias Becher

I found a great way to test this using Moq and FluentAssertions. Suppose your unit under test looks like this:

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

Your test would look like this (assuming MS Test):

你的测试看起来像这样(假设 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();
    }
}

This works great for my purposes.

这对我的目的很有用。