Mocking Database transactions?

Viewed 20933

I have a pair of tables with a parent/child relationship - incident and incidentDetails. I have a viewmodel that contains information from both of these tables. And I have a business layer method that is passed an instance of the viewmodel that needs to update both tables.

So, in the method, I'm using EF6's new transaction mechanism:

using (var transaction = this.db.Database.BeginTransaction())
{
    try
    {
        // various database stuff
        this.db.SaveChanges();
        // more database stuff
        this.db.SaveChanges();
        // yet more database stuff
        this.db.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        transaction.Rollback();
        this.logger.logException(ex, "Exception caught in transaction, rolling back");
        throw;
    }
}

And so, my problem. How do I test this?

I'm using Microsoft's unit testing framework, with Moq, and I have had no trouble with mocking up DBContexts, and DbSet<>s, but I can't seem to figure out how to get around the transaction stuff.

If I don't attempt to mock the transaction, I get an InvalidOperationException:

"No connecting string named xxx could be found in the application config file."

Which makes perfect sense - there isn't an application config file, and there isn't any database.

But if I try to mock BeginTransaction(), I get initialization errors: NotSupportedException:

"Invalid setup on a non-virtual member: m => m.Database.BeginTransaction".

And that got me chasing into the weeds, looking at decompiles of the .NET methods, trying to identify some class that might derive from a usable interface, or something, where I could somehow inject a mocking object.

I'm not trying to unit-test MS's transactional code - I just want to make sure that the appropriate changes are made to the appropriate records in each of the tables. But as it sits, it looks like this is non-testable, and that any method that uses transactions is non-testable. And that's just a pain.

I've Googled around, and not found anything of use. Has anyone run into this issue? Anyone have ideas on how to proceed?

6 Answers

We implemented ivaylo-pashov's solution along with this code:

//Dependency Injection
public static void RegisterTypes(IUnityContainer container)
        {
            // Register manager mappings.
            container.RegisterType<IDatabaseContextProvider, EntityContextProvider>(new PerResolveLifetimeManager());
        }
    }

//Test Setup
        /// <summary>
        ///     Mocked <see cref="IrdEntities" /> context to be used in testing.
        /// </summary>
        private Mock<CCMSEntities> _irdContextMock;
        /// <summary>
        ///     Mocked <see cref="IDatabaseContextProvider" /> context to be used in testing.
        /// </summary>
        private Mock<IDatabaseContextProvider> _EntityContextProvider;
...

            _irdContextMock = new Mock<CCMSEntities>();
            _irdContextMock.Setup(m => m.Outbreaks).Returns(new Mock<DbSet<Outbreak>>().SetupData(_outbreakData).Object);
            _irdContextMock.Setup(m => m.FDI_Number_Counter).Returns(new Mock<DbSet<FDI_Number_Counter>>().SetupData(new List<FDI_Number_Counter>()).Object);

            _EntityContextProvider = new Mock<IDatabaseContextProvider>();
            _EntityContextProvider.Setup(m => m.Context).Returns(_irdContextMock.Object);

            _irdOutbreakRepository = new IrdOutbreakRepository(_EntityContextProvider.Object, _loggerMock.Object);

// Usage in the Class being tested:
//Constructor
        public IrdOutbreakRepository(IDatabaseContextProvider entityContextProvider, ILogger logger)
        {
            _entityContextProvider = entityContextProvider;
            _irdContext = entityContextProvider.Context;
            _logger = logger;
        }

        /// <summary>
        ///     The wrapper for the Entity Framework context and transaction.
        /// </summary>
        private readonly IDatabaseContextProvider _entityContextProvider;

        // The usage of a transaction that automatically gets mocked because the return type is void.
        _entityContextProvider.BeginTransaction();
...
Related