【问题标题】:Entity-Framework. Testing that SaveChanges is present and called in the correct place in a method实体框架。测试 SaveChanges 是否存在并在方法中的正确位置调用
【发布时间】:2014-02-28 13:47:45
【问题描述】:

我有一个类似以下的类,我想对其进行单元测试:

public class AddUserCommand
{
    IDbContext dbContext;

    public AddUserCommand(IDbContext context)   
    {
    dbContext = context;
    }

    public void Execute()
    {
        dbContext.Users.Add(new User());
        dbContext.SaveChanges();
    }
}

最终我需要在使用真正的sql数据库连接时测试Execute方法是否将新用户持久化到数据库中。但是对于我的单元测试,我显然想使用某种模拟对象。 在我的测试中,我可以制作一个模仿行为的模拟 IDbContext,并且一切正常。在运行 Execute 方法后,我可以测试模拟上下文是否包含新用户。

我的问题是,当使用模拟上下文时,如果我不调用 SaveChanges 方法,测试将通过。这是因为模拟上下文不需要进行 sql 查询来实际持久化数据。它在没有调用 SaveChanges 的情况下“持久”,因为 Users 集合代表持久存储。

为了检查 SaveChanges 是否被调用,许多在线资源 (例如: http://msdn.microsoft.com/en-us/library/ff714955.aspxhttp://msdn.microsoft.com/en-gb/data/dn314431.aspx) 说要在模拟上下文中添加这样的内容:

public class MockDbContext : IDbContext
{
    boolean saved;
    public void SaveChanges {
        saved = true;
    }
}

然后在调用Execute方法后测试保存的变量是否为真。但是,我发现这种方法缺乏的是,如果 Execute 方法这样做,这样的测试将通过:

public void Execute()
{
    dbContext.SaveChanges();
    dbContext.Users.Add(new User());
}

这当然不会保存任何更改,因为它完成得太早了。 我相信像 RhinoMocks 这样的 mocking 框架允许你测试方法调用到 mock 上下文的顺序,但我也读到这不是最佳实践(你应该测试结果,而不是实现的细节)。

问题在于模拟上下文并不能完全复制真实 DbContext 的作用。

所以我的问题是:是否有一种标准方法来模拟实体框架 DbContext,使得对象的任何添加或删除仅在调用 SaveChanges 时提交给模拟?或者这不是通常测试的东西?

【问题讨论】:

    标签: c# entity-framework unit-testing mocking


    【解决方案1】:

    您应该可以使用Moq 框架来做到这一点:

    // Counters to verify call order
    int callCount = 0;
    int addUser = 0;
    int saveChanges = 0;
    
    // use Moq to create a mock IDbContext.
    var mockContext = new Mock<IDbContext>();
    
    // Register callbacks for the mocked methods to increment our counters.
    mockContext.Setup(x => x.Users.Add(It.IsAny<User>())).Callback(() => addUser = callCount++);
    mockContext.Setup(x => x.SaveChanges()).Callback(() => saveChanges = callCount++);
    
    // Create the command, providing it the mocked IDbContext and execute it
    var command = new AddUserCommand(mockContext.Object);
    command.Execute();
    
    // Check that each method was only called once.
    mockContext.Verify(x => x.Users.Add(It.IsAny<User>()), Times.Once());
    mockContext.Verify(x => x.SaveChanges(), Times.Once());
    
    // check the counters to confirm the call order.
    Assert.AreEqual(0, addUser);
    Assert.AreEqual(1, saveChanges);
    

    按照有关此答案的 cmets,似乎有些人缺少单元测试的要点以及在代码中使用抽象的目的。

    您在这里所做的是验证 AddUserCommand 的行为,仅此而已 - 您正在确认 AddUserCommand 类正在添加用户并将更改保存在上下文中。

    使用IDbContext 接口的原因是您可以在没有已知状态下可用的数据库的情况下单独测试AddUserCommand 类。你不需要测试真正的DbContext 的实现,因为它应该有自己的单元测试,也可以单独覆盖。

    您可能想要创建一个集成测试,您将在其中使用真正的DbContext 并确认一条记录进入数据库,但这不是单元测试可以。

    【讨论】:

    • +1,但我希望有一种方法可以测试更改是否已保存,而不是调用 SaveChanges。后者感觉像是测试实现细节而不是结果。
    • @JohnSaunders - 这听起来更像是您想要集成测试而不是单元测试。
    • 不,我想要一个单元测试。但是SaveChanges 被调用的事实似乎是对实现细节的测试。我可以改为测试 EF 作为SaveChanges 被调用的result 所做的事情。这也将允许进行测试以确保在数据发生更改时执行UPDATE,或者在删除数据时执行DELETE
    • 谢谢,答案确实解决了订单部分的问题。但正如 John 所说,mock 的行为与真实 dbcontext 的行为仍然存在问题,因此它没有测试真实数据库上会发生什么。而且我不认为要求它使其成为集成测试,因为我只想测试代码是否将用户置于上下文中(无论是否是模拟上下文)。如果实体框架 DbContext 不是完全可模拟的,那么使用它的任何代码似乎都不是完全可单元测试的。
    • @IanDangerRobertson 我认为 TrevorPilley 想说的是,您需要设置多个单元测试,每个测试都针对它自己的场景:1)存在处于 Add 状态的实体,并且您有一个SaveChanges 中 x 的结果,接下来代码应该做什么? 2) y 状态中有实体,你得到了 z 的结果,接下来代码应该做什么。不要编写代码让这些事情真正发生在你的起订量中,让测试从“已经发生的事情开始,现在是什么”
    猜你喜欢
    • 2012-03-24
    • 1970-01-01
    • 1970-01-01
    • 2011-08-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-30
    • 2012-07-17
    相关资源
    最近更新 更多