我试图理解单元测试中的模拟并将单元测试过程集成到我的项目中。所以我一直走过几个教程并重构我的代码以支持模拟,无论如何,我无法通过测试,因为我试图测试的数据库方法是使用事务,但是在创建事务时,我得到了
底层提供程序在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()
任何想法如何解决这个问题?
答案 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
。这应该使模拟方式更简单,因为您可以设计IDataContext
和DataContextAdapter
来隐藏实际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()