简体   繁体   English

越过实体框架 BeginTransaction

[英]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所以我一直在浏览几个教程并重构我的代码以支持模拟,无论如何,我无法通过测试,因为我尝试测试的 DB 方法正在使用事务,但是在创建事务时,我得到

The underlying provider failed on Open.底层提供程序在 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()非虚拟(在 VB 中可覆盖)成员上的无效设置:conn => conn.Database.BeginTransaction()

Any ideas how to solve this?任何想法如何解决这个问题?

As the second error message says, Moq can't mock non-virtual methods or properties, so this approach won't work.正如第二条错误消息所说,Moq 不能模拟非虚拟方法或属性,因此这种方法不起作用。 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.这个想法是创建一个与DataContext交互的适配器(一个实现某些接口的包装类),并通过该接口执行所有数据库活动。 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 .以前直接使用DataContext所有代码现在都应该使用IDataContext ,当程序运行时它应该是DataContextAdapter ,但在测试中,您可以轻松模拟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 .这也应该使IDataContext方式更简单,因为您可以设计IDataContextDataContextAdapter来隐藏实际DataContext一些复杂性。

You can find a pretty good solution here .你可以在这里找到一个很好的解决方案。

In short, you need to create proxy class for DbContextTransaction and use it instead of an original one.简而言之,您需要为DbContextTransaction创建代理类并使用它代替原来的代理类。 So that you can mock your proxy and test your method with BeginTransaction() .这样您就可以模拟您的代理并使用BeginTransaction()测试您的方法。

PS.附注。 In article which I linked above, author forgot about the virtual keyword for BeginTransaction() method placed in dbContext class:在我上面链接的文章中,作者忘记了放置在 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);
}

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();你的模拟/伪造仍然需要返回一些东西,所以 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 .通常我会设置我的适配器的假冒在那个时候从BeginTransaction()返回一个接口(这样我也可以假冒返回的对象),但是由BeginTransaction()返回的DbContextTransaction只实现了IDisposable所以没有接口可以让我可以访问DbContextTransactionRollbackCommit方法。

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).此外, DbContextTransaction没有公共构造函数,所以我不能只是新建一个它的实例来返回(即使我可以,它也不是理想的,因为我无法检查对提交或回滚的调用)交易)。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM