Search code examples
c#entity-frameworkunit-testingtestingmoq

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?


Solution

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