如何在N层架构中模拟Entity Framework

5
我有一个使用Entity Framework(Code-First方法)的N层应用程序。现在我想自动化一些测试。我正在使用Moq框架。我在编写测试时遇到了一些问题。也许我的架构是错误的?我指的是,我编写的组件没有良好的隔离,因此无法进行测试。我真的不喜欢这样...或者,也许我只是无法正确地使用moq框架。
我让您看看我的架构:

enter image description here

在每个级别上,我都会在类的构造函数中注入我的context
外观模式:
public class PublicAreaFacade : IPublicAreaFacade, IDisposable
{
    private UnitOfWork _unitOfWork;

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

业务逻辑层:

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是我要插入到Entity Framework中的实体。它不是模拟的,我也无法注入它。因此,即使我传递了模拟上下文(IDataContext),当我尝试插入一个未模拟的实体时,我的测试就会失败!

这引发了我对自己是否有错误架构的疑问!

那么,是架构有问题还是我使用moq的方式有问题?

感谢您的帮助。

更新:

以下是我如何测试我的代码。例如,我想测试一笔付款的跟踪记录。

以下是测试代码:

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

1
请问您能否展示一下有关插入/更新实体的测试,并解释它是如何失败的?同时,提供被测试的代码将会很有帮助。 - Good Night Nerd Pride
我已经更新了我的问题,并附上了一个例子。 - Simone
4个回答

2
你的架构看起来不错,但实现有缺陷。它存在"泄露抽象"问题。
在你的图表中,“Façade”层仅依赖于“BLL”,但是当你查看“PublicAreaFacade”的构造函数时,你会发现它实际上直接依赖于“Repository”层的一个接口。
public PublicAreaFacade(IDataContext context)
{
    _unitOfWork = new UnitOfWork(context);
}

这是不应该的。它只应该以其直接依赖项作为输入--PaymentsManager或者--更好的方式--它的接口:

public PublicAreaFacade(IPaymentsManager paymentsManager)
{
    ...
}

结果是您的代码变得更加易于测试。 当您查看测试时,您会发现必须模拟系统的最内层(即和甚至其实体访问器>),尽管您正在测试系统的最外层之一(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,我同意你的看法,我不喜欢将其传递到门面中,但如果我不传递它,就会有两个问题:
  1. 如果上下文在 BLL 后面“隐藏”,我如何模拟上下文?如果您查看我的测试,我会模拟上下文,然后将其作为输入传递给门面;
  2. 一个门面可以调用不同的管理器。管理器使用上下文。如果我不将上下文作为输入传递给门面,那么如何为每个管理器实例化一个上下文?
- Simone
1
诀窍在于测试门面时不必模拟上下文。如果你测试门面的一个方法,你只需要测试门面本身正在做什么,而不是背景中隐含发生的事情——这就是模拟的定义。 - Good Night Nerd Pride
1
我建议你做的是依赖注入。在DI中,通常在应用程序的入口点创建完整的对象图。在你的情况下,这是REST服务端点。但你也可以使用像Microsoft Unity这样的DI框架,它可以使对象图的创建变得更加容易。 - Good Night Nerd Pride
谢谢,我已经在使用微软的Unity了,但是我还没有真正熟练。即使它能够工作,我知道我的Unity配置有些问题,但我还没有决定哪种方法是最好的来纠正它... - Simone

0

听起来你需要稍微改一下代码。Newing 东西会引入硬编码的依赖关系并使它们无法测试,因此尝试将它们抽象出来。也许你可以将所有与 EF 相关的内容隐藏在另一层后面,然后你只需要模拟那个特定的层,而不必触及 EF。


0

你可以使用这个开源框架进行单元测试,它很适合模拟实体框架的DbContext。

https://effort.codeplex.com/

尝试这个方法可以帮助您高效地模拟数据。


0
IUnitOfWork 传递到 Facade 或 BLL 层构造函数中,无论哪个直接调用工作单元。然后您可以在测试中设置 Mock<IUnitOfWork> 返回的内容。除了可能是 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 中测试您的代码。

对于插入测试,您应该有一个 Facade 方法,例如 CreatePayment,它接受一个 PrepareEasyPayPaymentRequest。在该 CreatePayment 方法内部,它应该通过单元操作引用 repo,可能是通过工作单元引用,如下所示:

var result = _unitOfWork.CreatePaymentRepoCall(request);
if (result == true)
{
    // yes!
} 
else
{
    // oh no!
}

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

您还可以测试是否按预期进行了插入调用,但除非该调用的参数涉及大量逻辑构建,否则通常不太有价值。


我理解你的意思,但不幸的是你的解决方案并不能解决我的问题。我已经有一个像 CreatePayment 这样的方法,它接受一个 PrepareEasyPayPaymentRequest。我可以更改我的门面(facade)以将 IUnitOfWork 传递到门面构造函数中...但问题仍然存在!问题在于,在门面内部,我创建了要插入数据库的实体(Entity Framework)。我实例化这个实体...我使用 new...这样实体就不是模拟的...所以我无法测试插入操作...就好像我应该将模拟实体传递给门面一样,但这肯定不是解决问题的正确方式。 - Simone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接