【问题标题】:How to mock Entity Framework in a N-Layer Architecture如何在 N 层架构中模拟实体框架
【发布时间】:2016-05-17 09:33:13
【问题描述】:

我有一个带有实体框架(代码优先方法)的 N 层应用程序。现在我想自动化一些测试。我正在使用 Moq 框架。我在编写测试时发现了一些问题。也许我的架构是错误的?错了,我的意思是我编写的组件没有很好地隔离,因此它们不可测试。我不太喜欢这个……或者,我根本无法正确使用起订量框架。

我让你看看我的架构:

在每个级别,我都会在类的构造函数中注入我的context

立面:

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
}

这里似乎一切正常!!!看起来我的架构是正确的。但是如果我想插入/更新一个实体呢?没有任何工作了!我解释原因:

如您所见,我将 *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 是我将插入到实体框架的实体。它没有被模拟,我无法注入它。因此,即使我通过了我的模拟上下文 (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;
}

最后我是如何编写存储库的:

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

【问题讨论】:

  • 能否请您向我们展示有关插入/更新实体的测试并解释它究竟是如何失败的?被测代码也会有所帮助。
  • 我用一个例子更新了我的问题

标签: entity-framework unit-testing mocking moq n-layer


【解决方案1】:

您的架构看起来不错,但实施存在缺陷。它正在泄露抽象

在您的图表中,Façade 层仅依赖于 BLL,但是当您查看 PublicAreaFacade 的构造函数时,您会发现实际上它有一个直接的Repository 层对接口的依赖:

public PublicAreaFacade(IDataContext context)
{
    _unitOfWork = new UnitOfWork(context);
}

这不应该。它应该只将其直接依赖项作为输入——PaymentsManager 或者——甚至更好——它的接口:

public PublicAreaFacade(IPaymentsManager paymentsManager)
{
    ...
}

结果是您的代码变得方式更可测试。当您现在查看测试时,您会发现您必须模拟系统的最内层(即IDataContext 甚至它的实体访问器Set&lt;TEntity&gt;),尽管您正在测试最外层之一 您的系统(PublicAreaFacade 类)。

如果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>()))
}

【讨论】:

  • unitOfWork 包含所有必要的管理器,它们以一种懒惰的方式实例化。关于IDataContext 我同意你的观点,我不喜欢我将它传递到门面内部,但是如果我不通过它我有 2 个问题: 1. 如果上下文“隐藏”在 BLL 后面,我该如何模拟上下文?如果您看到我的测试,我会模拟上下文,然后将其作为输入传递给外观; 2. 1个Facade可以调用不同的管理器。管理人员使用上下文。如果我没有将输入中的上下文传递给外观,我如何才能为每个经理只实例化一个上下文?
  • 1) 诀窍是在测试外观时不必模拟上下文。如果你测试一个外观的方法,你只想测试外观本身在做什么,而不是在后台隐式发生什么——这就是模拟的定义。
  • 2) 我建议你做的就是依赖注入。在 DI 中,您通常在应用程序的入口点创建完整的对象图。在您的情况下,这是 REST 服务端点。但是您也可以使用像 Micorsoft 的 Unity 这样的 DI 框架,它使对象图的创建变得更加容易。
  • 谢谢,我已经在用微软的Unity了,但肯定我还没有真正练习过...即使它有效,我知道我的Unity配置有问题但我还没有决定然而,这是纠正它的最佳方法......
【解决方案2】:

IUnitOfWork 传递给Facade 或BLL 层构造函数,无论哪个直接调用工作单元。然后您可以设置 Mock&lt;IUnitOfWork&gt; 在您的测试中返回的内容。除了 repo 构造函数和工作单元之外,您不需要将 IDataContext 传递给所有内容。

例如,如果 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!
}

您想要模拟单元测试的是此创建/插入 repo 调用返回 true 或 false,因此您可以在 repo 调用完成后测试代码分支。

您还可以测试插入调用是否按预期进行,但这通常没有那么有价值,除非该调用的参数在构建它们时涉及大量逻辑。

【讨论】:

  • 我明白......但不幸的是你的解决方案不能解决我的问题..我已经有一个像CreatePayment这样的方法,它可以与PrepareEasyPayPaymentRequest相匹配。我可以更改我的外观以将IUnitOfWork 传递给外观构造函数.. 但问题仍然存在!问题是在外观内部我创建了实体(实体框架)以插入数据库中。我实例化实体......我做了一个new......这样实体没有被模拟......所以我无法测试插入......就像我应该将模拟实体传递给Facade,但这肯定是不是解决问题的正确方法
【解决方案3】:

听起来您需要稍微更改代码。新事物引入了硬编码的依赖关系并使它们无法测试,因此请尝试将它们抽象掉。也许您可以将与 EF 相关的所有内容隐藏在另一个层后面,那么您所要做的就是模拟该特定层层并且永远不要接触 EF。

【讨论】:

    【解决方案4】:

    您可以使用这个开源框架进行单元测试,它可以很好地模拟实体框架 dbcontext

    https://effort.codeplex.com/

    试试这个将帮助你有效地模拟你的数据。

    【讨论】:

      猜你喜欢
      • 2012-03-04
      • 2013-10-12
      • 1970-01-01
      • 1970-01-01
      • 2012-12-17
      • 2011-10-09
      • 1970-01-01
      • 2015-07-30
      • 2023-03-27
      相关资源
      最近更新 更多