【问题标题】:Getting past entity framework BeginTransaction获取过去的实体框架 BeginTransaction
【发布时间】:2015-09-13 20:05:45
【问题描述】:

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

底层提供程序在打开时失败。

没有事务一切正常。

我目前拥有的代码是:

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

但这不起作用,失败:

非虚拟(在 VB 中可覆盖)成员上的设置无效:conn => conn.Database.BeginTransaction()

有什么办法解决这个问题吗?

【问题讨论】:

    标签: c# entity-framework unit-testing testing moq


    【解决方案1】:

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

    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,但在测试中,您可以轻松地模拟IDataContext。这也应该使模拟方式更简单,因为您可以设计IDataContextDataContextAdapter 来隐藏实际DataContext 的一些复杂性。

    【讨论】:

    • 这个设计模式很有意义,我会努力去理解它。虽然我仍然无法让它工作,因为我无法模拟DbContextTransaction,因为它只有内部构造函数,这对Moq 来说似乎是个问题。无论如何,测试执行并不真正依赖于它的工作,如果我在没有调试的情况下运行测试,除非我明确检查是否引发了任何异常,否则它们会通过。这很好。感谢您的洞察力,除非出现更好的情况,否则我会接受这一点。 (y)
    • 您可以在那里使用相同的技巧 - 创建一个由DbContextTransactionAdapter 实现的IDbContextTransaction 接口,它包装了一个实际的DbContextTransaction - 并在您的测试中模拟IDbContextTransaction。这有点麻烦,但这个技巧通常会使使用库代码的代码更容易测试,并且与库的耦合度更低。
    • 当我做 Mock.Of 时,最近让我感到惊讶的一件事是,mock 的 Database 属性使用 app.config 中的连接信息创建了一个具体的数据库对象。因此,任何 Database.ExecuteSql 都可能会针对该数据库运行。还从 BeginTransaction 返回了一个具体的事务。这是怎么发生的,一个模拟返回一个真实的东西?
    • 调试上面很明显,mocker 必须调用默认构造函数,它初始化真正的 DbContext。无论如何,我应该使用接口。
    • @RobKent:很好的观察!
    【解决方案2】:

    我已经尝试过包装器/适配器的方法,但是当你去测试代码时遇到了一个问题:

    using (var transaction = _myAdaptor.BeginTransaction())
    

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

    通常我会设置我的适配器的假冒在那个时候从BeginTransaction()返回一个接口(所以我也可以伪造返回的对象),但是BeginTransaction()返回的DbContextTransaction只实现IDisposable所以没有接口可以让我访问DbContextTransactionRollbackCommit 方法。

    此外,DbContextTransaction 没有公共构造函数,所以我不能只新建它的一个实例来返回(即使我可以,它也不是理想的,因为我不能检查调用提交或回滚事务)。

    所以,最后我采取了一种稍微不同的方法,完全创建了一个单独的类来管理事务:

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

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

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

    【讨论】:

      【解决方案3】:

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

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

      PS。在我上面链接的文章中,作者忘记了 dbContext 类中 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);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多