如何在 N 层架构中模拟 Entity Framework
How to mock Entity Framework in a N-Layer Architecture
我有一个 N 层应用程序 Entity Framework(代码优先方法)。现在我想自动化一些测试。我正在使用 Moq 框架。我发现有关编写测试的一些问题。也许我的架构是错误的?错了,我的意思是我编写的组件没有很好地隔离,因此它们不可测试。我不太喜欢这个...或者,我根本无法正确使用 moq 框架。
我让你看看我的架构:
在每个级别我都将我的 context
注入到 class 的构造函数中。
门面:
public class PublicAreaFacade : IPublicAreaFacade, IDisposable
{
private UnitOfWork _unitOfWork;
public PublicAreaFacade(IDataContext context)
{
_unitOfWork = new UnitOfWork(context);
}
}
BLL:
public abstract class BaseManager
{
protected IDataContext Context;
public BaseManager(IDataContext context)
{
this.Context = context;
}
}
存储库:
public class Repository<TEntity>
where TEntity : class
{
internal PublicAreaContext _context;
internal DbSet<TEntity> _dbSet;
public Repository(IDataContext context)
{
this._context = context as PublicAreaContext;
}
}
IDataContext
是我的DbContext实现的接口:
public partial class PublicAreaContext : DbContext, IDataContext
现在,我如何模拟 EF
以及如何编写测试:
[TestInitialize]
public void Init()
{
this._mockContext = ContextHelper.CreateCompleteContext();
}
其中 ContextHelper.CreateCompleteContext()
是:
public static PublicAreaContext CreateCompleteContext()
{
//Here I mock my context
var mockContext = new Mock<PublicAreaContext>();
//Here I mock my entities
List<Customer> customers = new List<Customer>()
{
new Customer() { Code = "123455" }, //Customer with no invoice
new Customer() { Code = "123456" }
};
var mockSetCustomer = ContextHelper.SetList(customers);
mockContext.Setup(m => m.Set<Customer>()).Returns(mockSetCustomer);
...
return mockContext.Object;
}
下面是我如何编写测试:
[TestMethod]
public void Success()
{
#region Arrange
PrepareEasyPayPaymentRequest request = new PrepareEasyPayPaymentRequest();
request.CodiceEasyPay = "128855248542874445877";
request.Servizio = "MyService";
#endregion
#region Act
PublicAreaFacade facade = new PublicAreaFacade(this._mockContext);
PrepareEasyPayPaymentResponse response = facade.PrepareEasyPayPayment(request);
#endregion
#region Assert
Assert.IsTrue(response.Result == it.MC.WebApi.Models.ResponseDTO.ResponseResult.Success);
#endregion
}
这里 似乎一切正常!!!看起来我的架构是正确的。但是如果我想 insert/update 一个实体呢?没有什么工作了!我解释一下原因:
如您所见,我将一个 *Request
对象(它是 DTO)传递给外观,然后在我的 TOA 中,我从 DTO 的属性生成我的实体:
private PaymentAttemptTrace CreatePaymentAttemptTraceEntity(string customerCode, int idInvoice, DateTime paymentDate)
{
PaymentAttemptTrace trace = new PaymentAttemptTrace();
trace.customerCode = customerCode;
trace.InvoiceId = idInvoice;
trace.PaymentDate = paymentDate;
return trace;
}
PaymentAttemptTrace
是我将插入到 Entity Framework 的实体。它没有被模拟,我无法注入它。因此,即使我通过了我的模拟上下文 (IDataContext),当我尝试插入一个未模拟的实体时,我的测试也会失败!
这里关于我架构错误的疑问已经提出来了!
那么,怎么了?架构或我使用最小起订量的方式?
感谢您的帮助
更新
下面是我测试代码的方式。例如,我想测试一笔付款的踪迹。
这里是测试:
[TestMethod]
public void NoPaymentDate()
{
TracePaymentAttemptRequest request = new TracePaymentAttemptRequest();
request.AliasTerminale = "MyTerminal";
//...
//I create my request object
//You can see how I create _mockContext above
PublicAreaFacade facade = new PublicAreaFacade(this._mockContext);
TracePaymentAttemptResponse response = facade.TracePaymentAttempt(request);
//My asserts
}
这里是门面:
public TracePaymentAttemptResponse TracePaymentAttempt(TracePaymentAttemptRequest request)
{
TracePaymentAttemptResponse response = new TracePaymentAttemptResponse();
try
{
...
_unitOfWork.PaymentsManager.SavePaymentAttemptResult(
easyPay.CustomerCode,
request.CodiceTransazione,
request.EsitoPagamento + " - " + request.DescrizioneEsitoPagamento,
request.Email,
request.AliasTerminale,
request.NumeroContratto,
easyPay.IdInvoice,
request.TotalePagamento,
paymentDate);
_unitOfWork.Commit();
response.Result = ResponseResult.Success;
}
catch (Exception ex)
{
response.Result = ResponseResult.Fail;
response.ResultMessage = ex.Message;
}
return response;
}
这里是我如何开发 PaymentsManager
:
public PaymentAttemptTrace SavePaymentAttemptResult(string customerCode, string transactionCode, ...)
{
//here the problem... PaymentAttemptTrace is the entity of entity framework.. Here i do the NEW of the object.. It should be injected, but I think it would be a wrong solution
PaymentAttemptTrace trace = new PaymentAttemptTrace();
trace.customerCode = customerCode;
trace.InvoiceId = idInvoice;
trace.PaymentDate = paymentDate;
trace.Result = result;
trace.Email = email;
trace.Terminal = terminal;
trace.EasypayCode = transactionCode;
trace.Amount = amount;
trace.creditCardId = idCreditCard;
trace.PaymentMethod = paymentMethod;
Repository<PaymentAttemptTrace> repository = new Repository<PaymentAttemptTrace>(base.Context);
repository.Insert(trace);
return trace;
}
到底我是怎么写repository的:
public class Repository<TEntity>
where TEntity : class
{
internal PublicAreaContext _context;
internal DbSet<TEntity> _dbSet;
public Repository(IDataContext context)
{
//the context is mocked.. Its type is {Castle.Proxies.PublicAreaContextProxy}
this._context = context as PublicAreaContext;
//the entity is not mocked. Its type is {PaymentAttemptTrace} but should be {Castle.Proxies.PaymentAttemptTraceProxy}... so _dbSet result NULL
this._dbSet = this._context.Set<TEntity>();
}
public virtual void Insert(TEntity entity)
{
//_dbSet is NULL so "Object reference not set to an instance of an object" exception is raised
this._dbSet.Add(entity);
}
}
将 IUnitOfWork
传递到 Facade 或 BLL 层构造函数中,无论哪个直接调用工作单元。然后您可以设置 Mock<IUnitOfWork>
在您的测试中返回的内容。您不需要将 IDataContext
传递给除了 repo 构造函数和工作单元之外的所有内容。
例如,如果 Facade 有一个方法 PrepareEasyPayPayment
通过 UnitOfWork
调用进行 repo 调用,则像这样设置模拟:
// Arrange
var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.Setup(x => x.PrepareEasyPayPaymentRepoCall(request)).Returns(true);
var paymentFacade = new PaymentFacade(unitOfWork.Object);
// Act
var result = paymentFacade.PrepareEasyPayPayment(request);
那么您已经模拟了数据调用并且可以更轻松地在 Facade 中测试您的代码。
对于插入测试,您应该有一个像 CreatePayment
这样的 Facade 方法,它需要一个 PrepareEasyPayPaymentRequest
。在那个 CreatePayment
方法中,它应该引用 repo,可能是通过工作单元,比如
var result = _unitOfWork.CreatePaymentRepoCall(request);
if (result == true)
{
// yes!
}
else
{
// oh no!
}
您要为单元测试模拟的是此 create/insert 回购调用 returns true 或 false,以便您可以在回购调用完成后测试代码分支。
您还可以测试插入调用是否按预期进行,但这通常没有那么重要,除非该调用的参数在构建过程中涉及很多逻辑。
听起来您需要稍微更改一下代码。新事物会引入硬编码的依赖关系并使它们无法测试,因此请尝试将它们抽象出来。也许您可以将与 EF 相关的所有内容隐藏在另一层之后,然后您所要做的就是模拟该特定层层,永远不要接触 EF。
您的架构看起来不错,但实现有缺陷。 泄漏抽象。
在你的图表中 Façade 层只依赖于 BLL 但是当你查看 PublicAreaFacade
的构造函数时你会看到实际上它直接依赖于 Repository 层的接口:
public PublicAreaFacade(IDataContext context)
{
_unitOfWork = new UnitOfWork(context);
}
这不应该。它应该只将它的直接依赖作为输入——PaymentsManager
或者——更好的是——它的一个接口:
public PublicAreaFacade(IPaymentsManager paymentsManager)
{
...
}
结果是您的代码变得 更易于测试。当你现在查看你的测试时,你会发现你必须模拟系统的最内层(即 IDataContext
甚至它的实体访问器 Set<TEntity>
),尽管你正在测试 one系统的最外层 (PublicAreaFacade
class)。
如果 PublicAreaFacade
仅依赖于 IPaymentsManager
:
,那么 TracePaymentAttempt
方法的单元测试会是这样的
[TestMethod]
public void CallsPaymentManagerWithRequestDataWhenTracingPaymentAttempts()
{
// Arrange
var pm = new Mock<IPaymentsManager>();
var pa = new PulicAreaFacade(pm.Object);
var payment = new TracePaymentAttemptRequest
{
...
}
// Act
pa.TracePaymentAttempt(payment);
// Assert that we call the correct method of the PaymentsManager with the data from
// the request.
pm.Verify(pm => pm.SavePaymentAttemptResult(
It.IsAny<string>(),
payment.CodiceTransazione,
payment.EsitoPagamento + " - " + payment.DescrizioneEsitoPagamento,
payment.Email,
payment.AliasTerminale,
payment.NumeroContratto,
It.IsAny<int>(),
payment.TotalePagamento,
It.IsAny<DateTime>()))
}
您可以使用这个开源框架进行单元测试,这对 mock 来说很好 entity framework dbcontext
试试这个将帮助您有效地模拟数据。
我有一个 N 层应用程序 Entity Framework(代码优先方法)。现在我想自动化一些测试。我正在使用 Moq 框架。我发现有关编写测试的一些问题。也许我的架构是错误的?错了,我的意思是我编写的组件没有很好地隔离,因此它们不可测试。我不太喜欢这个...或者,我根本无法正确使用 moq 框架。
我让你看看我的架构:
在每个级别我都将我的 context
注入到 class 的构造函数中。
门面:
public class PublicAreaFacade : IPublicAreaFacade, IDisposable
{
private UnitOfWork _unitOfWork;
public PublicAreaFacade(IDataContext context)
{
_unitOfWork = new UnitOfWork(context);
}
}
BLL:
public abstract class BaseManager
{
protected IDataContext Context;
public BaseManager(IDataContext context)
{
this.Context = context;
}
}
存储库:
public class Repository<TEntity>
where TEntity : class
{
internal PublicAreaContext _context;
internal DbSet<TEntity> _dbSet;
public Repository(IDataContext context)
{
this._context = context as PublicAreaContext;
}
}
IDataContext
是我的DbContext实现的接口:
public partial class PublicAreaContext : DbContext, IDataContext
现在,我如何模拟 EF
以及如何编写测试:
[TestInitialize]
public void Init()
{
this._mockContext = ContextHelper.CreateCompleteContext();
}
其中 ContextHelper.CreateCompleteContext()
是:
public static PublicAreaContext CreateCompleteContext()
{
//Here I mock my context
var mockContext = new Mock<PublicAreaContext>();
//Here I mock my entities
List<Customer> customers = new List<Customer>()
{
new Customer() { Code = "123455" }, //Customer with no invoice
new Customer() { Code = "123456" }
};
var mockSetCustomer = ContextHelper.SetList(customers);
mockContext.Setup(m => m.Set<Customer>()).Returns(mockSetCustomer);
...
return mockContext.Object;
}
下面是我如何编写测试:
[TestMethod]
public void Success()
{
#region Arrange
PrepareEasyPayPaymentRequest request = new PrepareEasyPayPaymentRequest();
request.CodiceEasyPay = "128855248542874445877";
request.Servizio = "MyService";
#endregion
#region Act
PublicAreaFacade facade = new PublicAreaFacade(this._mockContext);
PrepareEasyPayPaymentResponse response = facade.PrepareEasyPayPayment(request);
#endregion
#region Assert
Assert.IsTrue(response.Result == it.MC.WebApi.Models.ResponseDTO.ResponseResult.Success);
#endregion
}
这里 似乎一切正常!!!看起来我的架构是正确的。但是如果我想 insert/update 一个实体呢?没有什么工作了!我解释一下原因:
如您所见,我将一个 *Request
对象(它是 DTO)传递给外观,然后在我的 TOA 中,我从 DTO 的属性生成我的实体:
private PaymentAttemptTrace CreatePaymentAttemptTraceEntity(string customerCode, int idInvoice, DateTime paymentDate)
{
PaymentAttemptTrace trace = new PaymentAttemptTrace();
trace.customerCode = customerCode;
trace.InvoiceId = idInvoice;
trace.PaymentDate = paymentDate;
return trace;
}
PaymentAttemptTrace
是我将插入到 Entity Framework 的实体。它没有被模拟,我无法注入它。因此,即使我通过了我的模拟上下文 (IDataContext),当我尝试插入一个未模拟的实体时,我的测试也会失败!
这里关于我架构错误的疑问已经提出来了!
那么,怎么了?架构或我使用最小起订量的方式?
感谢您的帮助
更新
下面是我测试代码的方式。例如,我想测试一笔付款的踪迹。
这里是测试:
[TestMethod]
public void NoPaymentDate()
{
TracePaymentAttemptRequest request = new TracePaymentAttemptRequest();
request.AliasTerminale = "MyTerminal";
//...
//I create my request object
//You can see how I create _mockContext above
PublicAreaFacade facade = new PublicAreaFacade(this._mockContext);
TracePaymentAttemptResponse response = facade.TracePaymentAttempt(request);
//My asserts
}
这里是门面:
public TracePaymentAttemptResponse TracePaymentAttempt(TracePaymentAttemptRequest request)
{
TracePaymentAttemptResponse response = new TracePaymentAttemptResponse();
try
{
...
_unitOfWork.PaymentsManager.SavePaymentAttemptResult(
easyPay.CustomerCode,
request.CodiceTransazione,
request.EsitoPagamento + " - " + request.DescrizioneEsitoPagamento,
request.Email,
request.AliasTerminale,
request.NumeroContratto,
easyPay.IdInvoice,
request.TotalePagamento,
paymentDate);
_unitOfWork.Commit();
response.Result = ResponseResult.Success;
}
catch (Exception ex)
{
response.Result = ResponseResult.Fail;
response.ResultMessage = ex.Message;
}
return response;
}
这里是我如何开发 PaymentsManager
:
public PaymentAttemptTrace SavePaymentAttemptResult(string customerCode, string transactionCode, ...)
{
//here the problem... PaymentAttemptTrace is the entity of entity framework.. Here i do the NEW of the object.. It should be injected, but I think it would be a wrong solution
PaymentAttemptTrace trace = new PaymentAttemptTrace();
trace.customerCode = customerCode;
trace.InvoiceId = idInvoice;
trace.PaymentDate = paymentDate;
trace.Result = result;
trace.Email = email;
trace.Terminal = terminal;
trace.EasypayCode = transactionCode;
trace.Amount = amount;
trace.creditCardId = idCreditCard;
trace.PaymentMethod = paymentMethod;
Repository<PaymentAttemptTrace> repository = new Repository<PaymentAttemptTrace>(base.Context);
repository.Insert(trace);
return trace;
}
到底我是怎么写repository的:
public class Repository<TEntity>
where TEntity : class
{
internal PublicAreaContext _context;
internal DbSet<TEntity> _dbSet;
public Repository(IDataContext context)
{
//the context is mocked.. Its type is {Castle.Proxies.PublicAreaContextProxy}
this._context = context as PublicAreaContext;
//the entity is not mocked. Its type is {PaymentAttemptTrace} but should be {Castle.Proxies.PaymentAttemptTraceProxy}... so _dbSet result NULL
this._dbSet = this._context.Set<TEntity>();
}
public virtual void Insert(TEntity entity)
{
//_dbSet is NULL so "Object reference not set to an instance of an object" exception is raised
this._dbSet.Add(entity);
}
}
将 IUnitOfWork
传递到 Facade 或 BLL 层构造函数中,无论哪个直接调用工作单元。然后您可以设置 Mock<IUnitOfWork>
在您的测试中返回的内容。您不需要将 IDataContext
传递给除了 repo 构造函数和工作单元之外的所有内容。
例如,如果 Facade 有一个方法 PrepareEasyPayPayment
通过 UnitOfWork
调用进行 repo 调用,则像这样设置模拟:
// Arrange
var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.Setup(x => x.PrepareEasyPayPaymentRepoCall(request)).Returns(true);
var paymentFacade = new PaymentFacade(unitOfWork.Object);
// Act
var result = paymentFacade.PrepareEasyPayPayment(request);
那么您已经模拟了数据调用并且可以更轻松地在 Facade 中测试您的代码。
对于插入测试,您应该有一个像 CreatePayment
这样的 Facade 方法,它需要一个 PrepareEasyPayPaymentRequest
。在那个 CreatePayment
方法中,它应该引用 repo,可能是通过工作单元,比如
var result = _unitOfWork.CreatePaymentRepoCall(request);
if (result == true)
{
// yes!
}
else
{
// oh no!
}
您要为单元测试模拟的是此 create/insert 回购调用 returns true 或 false,以便您可以在回购调用完成后测试代码分支。
您还可以测试插入调用是否按预期进行,但这通常没有那么重要,除非该调用的参数在构建过程中涉及很多逻辑。
听起来您需要稍微更改一下代码。新事物会引入硬编码的依赖关系并使它们无法测试,因此请尝试将它们抽象出来。也许您可以将与 EF 相关的所有内容隐藏在另一层之后,然后您所要做的就是模拟该特定层层,永远不要接触 EF。
您的架构看起来不错,但实现有缺陷。 泄漏抽象。
在你的图表中 Façade 层只依赖于 BLL 但是当你查看 PublicAreaFacade
的构造函数时你会看到实际上它直接依赖于 Repository 层的接口:
public PublicAreaFacade(IDataContext context)
{
_unitOfWork = new UnitOfWork(context);
}
这不应该。它应该只将它的直接依赖作为输入——PaymentsManager
或者——更好的是——它的一个接口:
public PublicAreaFacade(IPaymentsManager paymentsManager)
{
...
}
结果是您的代码变得 更易于测试。当你现在查看你的测试时,你会发现你必须模拟系统的最内层(即 IDataContext
甚至它的实体访问器 Set<TEntity>
),尽管你正在测试 one系统的最外层 (PublicAreaFacade
class)。
如果 PublicAreaFacade
仅依赖于 IPaymentsManager
:
TracePaymentAttempt
方法的单元测试会是这样的
[TestMethod]
public void CallsPaymentManagerWithRequestDataWhenTracingPaymentAttempts()
{
// Arrange
var pm = new Mock<IPaymentsManager>();
var pa = new PulicAreaFacade(pm.Object);
var payment = new TracePaymentAttemptRequest
{
...
}
// Act
pa.TracePaymentAttempt(payment);
// Assert that we call the correct method of the PaymentsManager with the data from
// the request.
pm.Verify(pm => pm.SavePaymentAttemptResult(
It.IsAny<string>(),
payment.CodiceTransazione,
payment.EsitoPagamento + " - " + payment.DescrizioneEsitoPagamento,
payment.Email,
payment.AliasTerminale,
payment.NumeroContratto,
It.IsAny<int>(),
payment.TotalePagamento,
It.IsAny<DateTime>()))
}
您可以使用这个开源框架进行单元测试,这对 mock 来说很好 entity framework dbcontext
试试这个将帮助您有效地模拟数据。