越过实体框架BeginTransaction

时间:2015-09-13 20:05:45

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

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

  

底层提供程序在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);

但这不起作用,失败了:

  

非虚拟(VB中可覆盖)成员的无效设置:conn =&gt;   conn.Database.BeginTransaction()

任何想法如何解决这个问题?

3 个答案:

答案 0 :(得分:5)

正如第二条错误消息所示,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的一些复杂性。

答案 1 :(得分:0)

我已经尝试了包装器/适配器方法,但是遇到了当您随后去测试代码的问题:

using (var transaction = _myAdaptor.BeginTransaction())

您的模拟/伪造仍然需要返回某些内容,因此行transaction.Commit(); 仍然可以执行。

通常,我会将适配器的伪造设置为在那个时候从BeginTransaction()返回一个接口(这样我也可以伪造那个返回的对象),但是DbContextTransaction返回的BeginTransaction() }仅实现IDisposable,因此没有接口可以让我访问Rollback的{​​{1}}和Commit方法。

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

因此,最后,我采取了一种略有不同的方法,并创建了一个单独的类来共同管理交易:

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

答案 2 :(得分:0)

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

简而言之,您需要为def goForward(self): if self.header == "someTask": if "tasks" in self.data: # check if the request has "tasks" in the body if self.counter < len(self.data["tasks"]): # check the counter self.code = self.data["tasks"][self.counter].get("code") action = self.data["tasks"][self.counter].get("actionDescription") if "myCondition" in str(action): self.returnStatus = 1 # set status #the two rows below give the same result #self.popup_B = ActivityBox(self) #self.popup_B.open() Clock.schedule_once(self.step3) # run the method in a thread t1 = threading.Thread(target = self.step1) t1.start() while self.returnStatus != 0: time.sleep(1) Cloch.schedule_once(self.step2) def step1(self): ts1 = threading.Thread(target = self.timeConsumingMethod) ts1.start() ts1.join() self.returnStatus = 0 # change status when over return(self.returnStatus) def step2(self, *args): ts2 = threading.Thread(target = self.popup_A) ts2.start() def step3(self, *args): #set the popup structure self.popup = ActivityBox(self) self.popup.open() def popup_A(self): self.popup = MessageBox(self) t3 = threading.Thread(target = self.popup.open) t3.start() def do(self): self.counter = self.counter + 1 self.popup.dismiss() self.goForward() def cancel(self): self.counter = self.counter + 1 self.popup.dismiss() exit() 创建代理类,并使用它代替原始的代理类。这样您就可以模拟代理并使用DbContextTransaction测试您的方法。

PS。在我上面链接的文章中,作者忘记了放在dbContext类中的BeginTransaction()方法的virtual关键字:

BeginTransaction()