Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting past entity framework BeginTransaction

I am trying to make sense of mocking in unit testing and to integrate the unit testing process to my project. So I have been walking thru several tutorials and refactoring my code to support mocking, anyway, I am unable to pass the tests, because the DB method I am trying to test is using a transaction, but when creating a transaction, I get

The underlying provider failed on Open.

Without transaction everything works just fine.

The code I currently have is:

[TestMethod]
public void Test1()
{
    var mockSet = GetDbMock();
    var mockContext = new Mock<DataContext>();
    mockContext.Setup(m => m.Repository).Returns(mockSet.Object);

    var service = new MyService(mockContext.Object);
    service.SaveRepository(GetRepositoryData().First());
    mockSet.Verify(m => m.Remove(It.IsAny<Repository>()), Times.Once());
    mockSet.Verify(m => m.Add(It.IsAny<Repository>()), Times.Once());
    mockContext.Verify(m => m.SaveChanges(), Times.Once());
}

// gets the DbSet mock with one existing item
private Mock<DbSet<Repository>> GetDbMock()
{
    var data = GetRepositoryData();
    var mockSet = new Mock<DbSet<Repository>>();

    mockSet.As<IQueryable<Repository>>().Setup(m => m.Provider).Returns(data.Provider);
    // skipped for brevity
    return mockSet;
}

Code under test:

private readonly DataContext _context;
public MyService(DataContext ctx)
{
    _context = ctx;
}

public void SaveRepositories(Repository repo)
{
    using (_context)
    {
        // Here the transaction creation fails
        using (var transaction = _context.Database.BeginTransaction())
        {
            DeleteExistingEntries(repo.Id);
            AddRepositories(repo);
            _context.SaveChanges();
            transaction.Commit();
        }
    }
}

I was trying to mock the transaction part as well:

var mockTransaction = new Mock<DbContextTransaction>();
mockContext.Setup(x => x.Database.BeginTransaction()).Returns(mockTransaction.Object);

but this is not working, failing with:

Invalid setup on a non-virtual (overridable in VB) member: conn => conn.Database.BeginTransaction()

Any ideas how to solve this?

like image 726
Erki M. Avatar asked Sep 13 '15 20:09

Erki M.


People also ask

How do I rollback a transaction in Entity Framework?

A DbContextTransaction object provides Commit() and Rollback() methods to do commit and rollback on the underlying store transaction. This method requires an open underlying stored connection. This method opens a connection if it is not already open. This method will close the connection when Dispose () is called.

Does SaveChanges commit?

In Entity Framework, the SaveChanges() method internally creates a transaction and wraps all INSERT, UPDATE and DELETE operations under it. Multiple SaveChanges() calls, create separate transactions, perform CRUD operations and then commit each transaction.

Does EF core use transaction?

EF Core relies on database providers to implement support for System. Transactions. If a provider does not implement support for System.

How is EF transaction Management done?

What EF does by default. In all versions of Entity Framework, whenever you execute SaveChanges() to insert, update or delete on the database the framework will wrap that operation in a transaction. This transaction lasts only long enough to execute the operation and then completes.


2 Answers

As the second error message says, Moq can't mock non-virtual methods or properties, so this approach won't work. I suggest using the Adapter pattern to work around this. The idea is to create an adapter (a wrapper class that implements some interface) that interacts with the DataContext, and to perform all database activity through that interface. Then, you can mock the interface instead.

public interface IDataContext {
    DbSet<Repository> Repository { get; }
    DbContextTransaction BeginTransaction();
}

public class DataContextAdapter {
    private readonly DataContext _dataContext;

    public DataContextAdapter(DataContext dataContext) {
        _dataContext = dataContext;
    }

    public DbSet<Repository> Repository { get { return _dataContext.Repository; } }

    public DbContextTransaction BeginTransaction() {
        return _dataContext.Database.BeginTransaction();
    }
}

All of your code that previously used the DataContext directly should now use an IDataContext, which should be a DataContextAdapter when the program is running, but in a test, you can easily mock IDataContext. This should make the mocking way simpler too because you can design IDataContext and DataContextAdapter to hide some of the complexities of the actual DataContext.

like image 180
Aasmund Eldhuset Avatar answered Sep 28 '22 10:09

Aasmund Eldhuset


I've tried the wrapper/adapter approach, but came up against the problem that when you then go to test the code:

using (var transaction = _myAdaptor.BeginTransaction())

Your mock/fake still needs to return something so the line transaction.Commit(); can still execute.

Normally I'd set the fake of my adapter to return an interface from BeginTransaction() at that point (so I can fake that returned object too), but the DbContextTransaction returned by BeginTransaction() only implements IDisposable so there was no interface that could give me access to the Rollback and Commit methods of DbContextTransaction.

Furthermore, DbContextTransaction has no public constructor, so I couldn't just new up an instance of it to return either (and even if I could, it wouldn't be ideal as I couldn't then check for calls to commit or rollback the transaction).

So, in the end I took a slightly different approach and created a separate class altogether to manage the transaction:

using System;
using System.Data.Entity;

public interface IEfTransactionService
{
    IManagedEfTransaction GetManagedEfTransaction();
}

public class EfTransactionService : IEfTransactionService
{
    private readonly IFMDContext _context;

    public EfTransactionService(IFMDContext context)
    {
        _context = context;
    }

    public IManagedEfTransaction GetManagedEfTransaction()
    {
        return new ManagedEfTransaction(_context);
    }
}

public interface IManagedEfTransaction : IDisposable
{
    DbContextTransaction BeginEfTransaction();
    void CommitEfTransaction();
    void RollbackEfTransaction();
}

public class ManagedEfTransaction : IManagedEfTransaction
{
    private readonly IDataContext  _context;
    private DbContextTransaction _transaction;

    public ManagedEfTransaction(IDataContext  context)
    {
        _context = context;
    }

    /// <summary>
    /// Not returning the transaction here because we want to avoid any
    /// external references to it stopping it from being disposed by
    /// the using statement
    /// </summary>
    public void BeginEfTransaction()
    {
        _transaction = _context.Database.BeginTransaction();
    }

    public void CommitEfTransaction()
    {
        if (_transaction == null) throw new Exception("No transaction");

        _transaction.Commit();
        _transaction = null;
    }

    public void RollbackEfTransaction()
    {
        if (_transaction == null) throw new Exception("No transaction");

        _transaction.Rollback();
        _transaction = null;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // free managed resources
            if (_transaction != null)
            {
                _transaction.Dispose();
                _transaction = null;
            }
        }
    }
}

I then inject that service class into whatever classes need to use a transaction. For example, using the code from the original question:

private readonly DataContext _context;
private readonly IEfTransactionManager _transactionManager;

public MyService(DataContext ctx, IEfTransactionManager transactionManager)
{
    _context = ctx;
    _transactionManager = transactionManager;
}

public void SaveRepositories(Repository repo)
{
    using (_context)
    {
        // Here the transaction creation fails
        using (var managedEfTransaction = _transactionManager.GetManagedEfTransaction())
        {
            try
            {
                managedEfTransaction.BeginEfTransaction();

                DeleteExistingEntries(repo.Id);
                AddRepositories(repo);
                _context.SaveChanges();

                managedEfTransaction.CommitEfTransaction();
            }
            catch (Exception)
            {
                managedEfTransaction.RollbackEfTransaction();
                throw;
            }
        }
    }
}
like image 32
tomRedox Avatar answered Sep 28 '22 10:09

tomRedox