Mocking Database transactions?

asp.net-mvc-4 c# entity-framework moq unit-testing

Question

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?

1
25
12/27/2013 9:24:29 PM

Accepted Answer

Testing this kind of stuff is always complicated, but first of all you should ask yourself if you want to unit test your business logic or if you want to integration test your application.

If you want to unit test your logic, you basically shouldn't even try to mock entity framework, because you do not want to test EF, you just want to test your code, right? To do so, mock any data access object and only unit test your business logic.

But if you want to test if your data access layer works, e.g. if your code can handle all the CRUD operations you have implemented, you should do integration tests against a real database. Do not try to mock any data access objects (EF) in this case, simply run you tests against a test database or a sql-express localDB for example.

13
12/27/2013 9:00:50 PM

Popular Answer

You can wrap the context and the transaction in an Interface and then implement the interface by some provider class:

public interface IDbContextProvider
{
    YourContext Context { get; set; }
    DbContextTransaction DbTransaction { get; set; }
    void Commit();
    void Rollback();
    void BeginTransaction();
    void SaveChanges();
}

and then implement it:

public class EfContextProvider : IDbContextProvider
{
    public EfContextProvider(YourContext context)
    {
        Context = context;
    }
    public YourContext Context { set; get; }
    public DbContextTransaction DbTransaction { set; get; }

    public void Commit()
    {
        DbTransaction.Commit();
    }

    public void Rollback()
    {
        DbTransaction.Rollback();
    }

    public void BeginTransaction()
    {
        DbTransaction=Context.Database.BeginTransaction();
    }

    public void SaveChanges()
    {
        Context.SaveChanges();
    }
}

so now give your class the IDbContextProvider dependency and work with it (It has also the context inside) . Maybe substitute the using block with _contextProvider.BeginTransaction(); and then also _contextProvider.Commit(); or _contextProvider.Rollback();



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow