Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking Database transactions?

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?

like image 704
Jeff Dege Avatar asked Dec 27 '13 20:12

Jeff Dege


People also ask

What is mocking a database?

What is Database Mocking? Database Mocking is a technique that allows you to set the desired database state (for different tables) in your tests to let specific data sets ready for future test execution.

Why do we mock the database?

Mocking and stubbing are the cornerstones of having quick and simple unit tests. Mocks are useful if you have a dependency on an external system, file reading takes too long, the database connection is unreliable, or if you don't want to send an email after every test.

What is the benefit of mocking?

Without taking the mocks you will not know how good/bad your preparation is. You can take the mocks and make strategies, plan your study schedule and make your preparation to the mark. Practice makes man perfect and for competitive exams, mocks are what make a difference between any candidate and the topper.

What is C# mocking?

Mocking is a process that allows you to create a mock object that can be used to simulate the behavior of a real object. You can use the mock object to verify that the real object was called with the expected parameters, and to verify that the real object was not called with unexpected parameters.


Video Answer


3 Answers

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.

like image 132
MichaC Avatar answered Oct 08 '22 16:10

MichaC


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

like image 40
Ivaylo Pashov Avatar answered Oct 08 '22 15:10

Ivaylo Pashov


I have spent few hours trying to figure it out, I believed it can be done by MS Fakes directly without wrapper or new classe.

You need to do three steps:

  1. Create shim object for DbContextTransaction, and detour its Commit and Rollback methods to do nothing.
  2. Create shim object for Database. And detour its BeginTransaction method to return DbContextTransaction shim object created in step 1.
  3. Detour DbContext.Database property for all instances to return Database shim object created in step 2.

And that all.

    static void SetupDBTransaction()
    {
        System.Data.Entity.Fakes.ShimDbContextTransaction transaction = new System.Data.Entity.Fakes.ShimDbContextTransaction();
        transaction.Commit = () => { };
        transaction.Rollback = () => { };

        System.Data.Entity.Fakes.ShimDatabase database = new System.Data.Entity.Fakes.ShimDatabase();
        database.BeginTransactionIsolationLevel = (isolationLevel) =>{return transaction.Instance;};

        System.Data.Entity.Fakes.ShimDbContext.AllInstances.DatabaseGet = (@this) => { return database.Instance; };
    }
like image 3
Kaboo Avatar answered Oct 08 '22 17:10

Kaboo