簡體   English   中英

越過實體框架 BeginTransaction

[英]Getting past entity framework BeginTransaction

我試圖理解單元測試中的模擬並將單元測試過程集成到我的項目中。 所以我一直在瀏覽幾個教程並重構我的代碼以支持模擬,無論如何,我無法通過測試,因為我嘗試測試的 DB 方法正在使用事務,但是在創建事務時,我得到

底層提供程序在 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 => conn.Database.BeginTransaction()

任何想法如何解決這個問題?

正如第二條錯誤消息所說,Moq 不能模擬非虛擬方法或屬性,因此這種方法不起作用。 我建議使用適配器模式來解決這個問題。 這個想法是創建一個與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 這也應該使IDataContext方式更簡單,因為您可以設計IDataContextDataContextAdapter來隱藏實際DataContext一些復雜性。

你可以在這里找到一個很好的解決方案。

簡而言之,您需要為DbContextTransaction創建代理類並使用它代替原來的代理類。 這樣您就可以模擬您的代理並使用BeginTransaction()測試您的方法。

附注。 在我上面鏈接的文章中,作者忘記了放置在 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);
}

我已經嘗試了包裝器/適配器方法,但遇到了一個問題,當你去測試代碼時:

using (var transaction = _myAdaptor.BeginTransaction())

你的模擬/偽造仍然需要返回一些東西,所以 line 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;
            }
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM