通过 entity framework BeginTransaction

Getting past entity framework BeginTransaction

我正在尝试理解单元测试中的模拟并将单元测试过程集成到我的项目中。所以我一直在浏览几个教程并重构我的代码以支持模拟,无论如何,我无法通过测试,因为我试图测试的数据库方法正在使用事务,但是在创建事务时,我得到

The underlying provider failed on Open.

没有事务一切正常。

我目前的代码是:

[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;
}

被测代码:

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

我也在尝试模拟交易部分:

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

但这不起作用,失败:

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

有什么解决办法吗?

如第二条错误消息所述,Moq 无法模拟非虚方法或属性,因此这种方法行不通。我建议使用 Adapter pattern 来解决这个问题。这个想法是创建一个与 DataContext 交互的 适配器 (一个实现某些接口的包装器 class),并执行所有数据库 activity通过那个界面。然后,您可以改为模拟界面。

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

你以前直接使用 DataContext 的所有代码现在应该使用 IDataContext,当程序是 运行 时,它应该是 DataContextAdapter,但是在一个测试,你可以轻松mock IDataContext。这也应该使模拟方式更简单,因为您可以设计 IDataContextDataContextAdapter 来隐藏实际 DataContext.

的一些复杂性

我试过 wrapper/adapter 方法,但是遇到了问题,当你去测试代码时:

using (var transaction = _myAdaptor.BeginTransaction())

你的 mock/fake 仍然需要 return 一些东西所以行 transaction.Commit(); 仍然可以执行。

通常我会在那时将我的适配器的伪造设置为 return 来自 BeginTransaction() 的接口(这样我也可以伪造那个 returned 对象),但是DbContextTransaction returned by BeginTransaction() 只实现了 IDisposable 所以没有接口可以让我访问 RollbackCommit 方法 DbContextTransaction

此外,DbContextTransaction 没有 public 构造函数,所以我也不能只将它的一个实例新建到 return(即使我可以,它也不会这不太理想,因为我无法检查调用以提交或回滚事务)。

所以,最后我采取了稍微不同的方法并创建了一个单独的 class 来管理交易:

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;
            }
        }
    }
}

然后我将该服务 class 注入任何 classes 需要使用事务的地方。例如,使用原始问题中的代码:

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;
            }
        }
    }
}

你可以找到一个很好的解决方案here

简而言之,您需要为 DbContextTransaction 创建代理 class 并使用它来代替原始代理。这样您就可以模拟您的代理并使用 BeginTransaction().

测试您的方法

PS。在我上面链接的文章中,作者忘记了 dbContext class:

BeginTransaction() 方法的 virtual 关键字
// <summary>
/// When we call begin transaction. Our proxy creates new Database.BeginTransaction and gives DbContextTransaction's control to proxy.
/// We do this for unit test.
/// </summary>
/// <returns>Proxy which controls DbContextTransaction(Ef transaction class)</returns>
public virtual IDbContextTransactionProxy BeginTransaction()
{
   return new DbContextTransactionProxy(this);
}