[英]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
方式更簡單,因為您可以設計IDataContext
和DataContextAdapter
來隱藏實際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
所以沒有接口可以讓我可以訪問DbContextTransaction
的Rollback
和Commit
方法。
此外, 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.