Unit Testing the Use of TransactionScope
Asked Answered
K

6

35

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.

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

Unfortunately, the standard pattern for using TransactionScope is a follows:

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 TransactionScope is used appropriately according to the standard pattern?

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. It involves adding a CreateTransactionScope method 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, CreateTransactionScope would return an instance of DataLayerTransactionScope which would be a mockable facade into TransactionScope.

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

Kirkland answered 9/3, 2009 at 15:50 Comment(1)
Thank you So much for this valuable Answer ! i have one que. Can i use this with ES DB (NoSQL) ?Disconsider
E
28

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/

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:

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.

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:

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();
        }
    }
}
Edentate answered 9/3, 2009 at 22:7 Comment(7)
I was afraid of that. Tell me, how do you handle catching the instantiation of TransactionScope? Do you force the instantiation via some form of factory and mock the factory to emit the mocked TransactionScope?Kirkland
The ITransactionManager in this case is that factory, it has the CreateScope-method. This is a service that I would inject to classes dependent upon transaction handling, alternatively a service locator could be used.Flamen
Hey, I just stumbled on your edit. LegendTransactions looks great!Kirkland
I think your are doing a great job ... I will definitely check your work on codeplex ... strange the MS people has not put that in their framework ...Miscreance
Please, any good sample code using IEnlistmentNotification ? Thanks.Whicker
A NuGet package would be nice. I myself have created a factory abstraction for creating what I call IUnitOfWork interfaces but this is great too as it allows for actual enlistment using the System.Transactions library. I could log all the changes or something like that in my unit tests.Rosemaria
Thank you So much for this valuable Answer ! i have one que. Can i use this with ES DB ?Disconsider
P
6

I found a great way to test this using Moq and 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):

[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.

Picul answered 14/10, 2014 at 12:58 Comment(0)
B
4

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

Very dirty hack is to check that Transaction.Current is not 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.

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)

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

Bessel answered 9/3, 2009 at 19:7 Comment(3)
I may be stuck doing the null check; I hope there are other options, however. Regarding whether the test is a good thing or not... could you provide your opinion after all? Do you think I shouldn't bother determining if a transaction was created? Or is it the desire to mock that you disagree with?Kirkland
it's not the mocking. it's the insistence that consumers of this api use transactions. For a single query no explicit transaction is required. it also may cause issues for people if it causes the DTM to kick in (a pain I've suffered)Bessel
I'd also be wary about whether this test is a false sense of security. dealing with things that require transactions for correctness is hard. Simply having a transaction may not be enough...Bessel
P
3

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.

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.

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.

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

Phototype answered 9/3, 2009 at 15:56 Comment(5)
Unfortunately, I do not plan to integration test this portion of the system; during my unit tests, the data layer will be mocked and no connections to the database will occur. I can test that the various method calls I expect occur; what I worry about is whether the TransactionScope is created.Kirkland
Essentially, I want repeatable unit tests that I can frequently and quickly; testing that a row was inserted or not is not going to cut it for me. But thank you for the reply! :)Kirkland
I think it's repeatable and fast enough - just my opinion. It seems very deterministic - one will succeed, the other will not. This isn't really a persistence test but a service layer test. The database would NOT be mocked, but the service tier would be if I were doing this.Phototype
I see where you are coming from, but I don't think this fits with what I'm looking for by my question. I already have integration tests that test my data layer to my satisfaction. I now wish to unit test my business layer's use of the data layer. I do not wish to do this with an integration testKirkland
Again, I see what you are saying, and I might have done so were situations different, but the unfortunate fact is that (for various bureaucratic reasons) I cannot run this test against an actual database. I must mock without connectivity.Kirkland
D
0

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.

Doha answered 20/2, 2013 at 13:55 Comment(2)
Unit test sample ?Whicker
Don't have a sample, but I'd just call the function as normal, then verify that the scopeCalled boolean is trueDoha
I
0

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

And then use factory method with 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();
        }
    }
}
Ictinus answered 23/6, 2023 at 11:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.