C# 实体框架。测试SaveChanges是否存在并在方法中的正确位置调用

C# 实体框架。测试SaveChanges是否存在并在方法中的正确位置调用,c#,entity-framework,unit-testing,mocking,C#,Entity Framework,Unit Testing,Mocking,我有一个类,如下所示,我想进行单元测试: public class AddUserCommand { IDbContext dbContext; public AddUserCommand(IDbContext context) { dbContext = context; } public void Execute() { dbContext.Users.Add(new User()); dbCo

我有一个类,如下所示,我想进行单元测试:

public class AddUserCommand
{
    IDbContext dbContext;

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

    public void Execute()
    {
        dbContext.Users.Add(new User());
        dbContext.SaveChanges();
    }
}
最后,我需要测试在使用真正的sql数据库连接时,Execute方法是否将新用户持久化到数据库。但是对于我的单元测试,我显然想使用某种模拟对象。 在我的测试中,我可以创建一个模仿行为的模拟上下文,并且一切都正常。我可以在Execute方法运行后测试mock上下文是否包含新用户

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

为了检查是否调用了SaveChanges,许多联机源 (例如: 及) 假设在模拟上下文中添加如下内容:

public class MockDbContext : IDbContext
{
    boolean saved;
    public void SaveChanges {
        saved = true;
    }
}
然后在调用Execute方法后测试保存的变量是否为true。但是,我发现这种方法的不足之处在于,如果Execute方法执行了以下操作,那么这样的测试就会通过:

public void Execute()
{
    dbContext.SaveChanges();
    dbContext.Users.Add(new User());
}
这当然不会保存任何更改,因为这样做太早了。 我相信像Rhinomock这样的模拟框架允许您测试对模拟上下文的方法调用顺序,但我也了解到这不是最佳实践(您应该测试结果,而不是实现的细节)

问题是模拟上下文并不完全复制真正的DbContext将要做的事情


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

您应该能够使用框架来实现这一点:

// 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);
//验证呼叫顺序的计数器
int callCount=0;
int addUser=0;
int saveChanges=0;
//使用Moq创建模拟IDbContext。
var mockContext=new Mock();
//注册模拟方法的回调以增加计数器。
mockContext.Setup(x=>x.Users.Add(It.IsAny()).Callback(()=>addUser=callCount++);
mockContext.Setup(x=>x.SaveChanges()).Callback(()=>SaveChanges=callCount++);
//创建命令,为其提供模拟IDbContext并执行它
var command=newaddusercommand(mockContext.Object);
command.Execute();
//检查每个方法只调用一次。
验证(x=>x.Users.Add(It.IsAny()),Times.Once());
验证(x=>x.SaveChanges(),Times.Once());
//检查计数器以确认呼叫顺序。
aresequal(0,addUser);
AreEqual(1,saveChanges);
在对这个答案的评论之后,似乎有些人忽略了单元测试的要点以及在代码中使用抽象的目的

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

使用
IDbContext
接口的原因是,您可以单独测试
AddUserCommand
类,而无需已知状态下的可用数据库。您不需要测试real
DbContext
的实现,因为它应该有自己的单元测试来单独测试


您可能还希望创建一个集成测试,在该测试中使用真实的
DbContext
,并确认记录进入数据库,但这不是单元测试所做的。

+1,但我希望有一种方法来测试更改是否已保存,而不是调用
SaveChanges
。后者感觉像是针对实现细节而不是结果进行测试。@JohnSaunders-这听起来更像是您想要的是集成测试而不是单元测试。不,我想要的是单元测试。但是调用
SaveChanges
似乎是对实现细节的测试。我可以通过调用
SaveChanges
来测试EF的功能。这还允许进行测试,以确保在数据发生更改时执行
更新
,或者在删除数据时执行
删除
。谢谢,答案确实解决了问题的顺序部分。但是正如John所说,仍然存在一个问题,即mock的行为与真正的dbcontext不同,因此它没有测试在真正的数据库上会发生什么。我不认为需要这样做就意味着它是一个集成测试,因为我只想测试代码是否将用户放在上下文中(无论这是否是模拟上下文)。如果实体框架DbContext不是完全可模拟的,那么使用它的任何代码似乎都不是完全可单元测试的。@iandangerobertson我想TrevorPilley想说的是,您需要设置多个单元测试,每个都有自己的场景:1)存在处于添加状态的实体,并且您从SaveChanges获得了x的结果,接下来代码应该做什么?2) 有一些实体处于y状态,你得到了一个z的结果,接下来代码应该做什么。不要编写代码让这些事情在你的Moq中真正发生,让测试从“已经发生的事情,现在发生什么”开始