简体   繁体   English

对 TransactionScope 的使用进行单元测试

[英]Unit Testing the Use of TransactionScope

The preamble: I have designed a strongly interfaced and fully mockable data layer class that expects the business layer to create a TransactionScope when multiple calls should be included in a single transaction.序言:我设计了一个强接口且完全可模拟的数据层 class,它期望业务层在单个事务中包含多个调用时创建一个TransactionScope

The problem: I would like to unit test that my business layer makes use of a TransactionScope object when I expect it to.问题:我想对我的业务层使用TransactionScope object 进行单元测试。

Unfortunately, the standard pattern for using TransactionScope is 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.我无法检测到瞬态 object 已被实例化,更不用说模拟它以确定对其调用了一个方法。 Yet my goal for coverage implies that I must.然而,我的报道目标意味着我必须这样做。

The Question: How can I go about building unit tests that ensure TransactionScope is 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 TransactionScope pattern.最后的想法:我考虑过一种解决方案,它肯定会提供我需要的覆盖范围,但由于过于复杂且不符合标准TransactionScope模式而拒绝了它。 It involves adding a CreateTransactionScope method on my data layer object that returns an instance of TransactionScope .它涉及在我的数据层 object 上添加一个CreateTransactionScope方法,该方法返回一个TransactionScope实例。 But because TransactionScope contains constructor logic and non-virtual methods and is therefore difficult if not impossible to mock, CreateTransactionScope would return an instance of DataLayerTransactionScope which would be a mockable facade into TransactionScope .但是由于 TransactionScope 包含构造函数逻辑和非虚拟方法,因此即使不是不可能模拟也很困难,因此CreateTransactionScope将返回一个DataLayerTransactionScope的实例,该实例将是TransactionScope的一个可模拟外观。

While this might do the job it's complex and I would prefer to use the standard pattern.虽然这可能会完成这项工作,但它很复杂,我更喜欢使用标准模式。 Is there a better way?有没有更好的办法?

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.为遵循相同模式但可模拟/可存根的现有类创建抽象。

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

}

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.我在想也许我应该为此创建一个 CodePlex 项目,这样问题就可以一劳永逸地解决。 This is not the first time I've run into this.这不是我第一次遇到这种情况。

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

Ignoring whether this test is a good thing or not....忽略这个测试是否是一件好事......

Very dirty hack is to check that Transaction.Current is not null.非常肮脏的黑客是检查 Transaction.Current 不是 null。

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 .另一种选择是故意尝试创建一个新的 TransactionScope,其隔离级别与将/应该使用的任何内容和TransactionScopeOption.Required不兼容。 If this succeeds rather than throwing an ArgumentException there wasn't a transaction.如果这成功而不是抛出 ArgumentException 则没有事务。 This requires you to know that a particular IsolationLevel is unused (something like Chaos is a potential choice)这需要你知道一个特定的 IsolationLevel 是未使用的(像 Chaos 之类的东西是一个潜在的选择)

Neither of these two options is particularly pleasant, the latter is very fragile and subject to the semantics of TransactionScope remaining constant.这两个选项都不是特别令人愉快,后者非常脆弱并且受 TransactionScope 语义保持不变。 I would test the former rather than the latter since it is somewhat more robust (and clear to read/debug).我会测试前者而不是后者,因为它更健壮(并且易于阅读/调试)。

I found a great way to test this using Moq and FluentAssertions.我找到了一种使用 Moq 和 FluentAssertions 进行测试的好方法。 Suppose your unit under test looks like this:假设您的被测单元如下所示:

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.这对我的目的很有用。

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.第二个应该是执行 InsertFoo 操作然后在尝试 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.成功的测试将表明异常已被抛出,并且 Foo 和 Bar 对象都没有提交到数据库。

If both of these pass, I'd say that your TransactionScope is working as it should.如果这两个都通过,我会说您的 TransactionScope 正在正常工作。

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.然后,当您需要测试您的代码时,您继承被测 Class,扩展 function,因此您可以检测它是否被调用。

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.然后在 class 的可测试版本上执行与 TransactionScope 相关的测试。

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

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