模拟 Verify() 调用

Mock Verify() Invocation

我正在单元测试以查看是否调用了方法。

[Fact]
        public void Can_Save_Project_Changes()
        {
            //Arrange
            var user = new AppUser() { UserName = "JohnDoe", Id = "1" };
            Mock<IRepository> mockRepo = new Mock<IRepository>();
            Mock<UserManager<AppUser>> userMgr = GetMockUserManager();
            userMgr.Setup(x => x.FindByNameAsync(It.IsAny<string>())).ReturnsAsync(new AppUser() { UserName = "JohnDoe", Id = "1" });
            var contextUser = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
            }));
            Mock<ITempDataDictionary> tempData = new Mock<ITempDataDictionary>();
            ProjectController controller = new ProjectController(mockRepo.Object, userMgr.Object)
            {
                TempData = tempData.Object,
                ControllerContext = new ControllerContext
                {
                    HttpContext = new DefaultHttpContext() { User = contextUser }
                }
            };

            Project project = new Project()
            {
                Name = "Test",
                UserID = "1",
            };

            //Act
            Task<IActionResult> result = controller.EditProject(project);

            //Assert

            mockRepo.Setup(m => m.SaveProject(It.IsAny<Project>(), user));
            //This line still throws an error
            mockRepo.Verify(m => m.SaveProject(It.IsAny<Project>(), user));
            Assert.IsType<Task<IActionResult>>(result);
            var view = result.Result as ViewResult;
            Assert.Equal("ProjectCharts", view.ViewName);
            Assert.Equal("Project", view.Model.ToString());
        }

在调试时,我可以验证该方法确实在控制器中被调用,

//This controller line is touched walking through the code
repository.SaveProject(project, user);

//but this repo line is not touched
public void SaveProject(Project project, AppUser user)      

调试实际上并没有显示进入存储库方法。具体错误如下

Expected invocation on the mock at least once, but was never performed: m => m.SaveProject(, JohnDoe)

No setups configured. Performed invocations: IRepository.ProjectClass IRepository.SaveProjects(ProjectClass, JohnDoe)'

当我进行实际的集成测试时,SaveProject 方法在存储库中被触及并且似乎工作正常。我也尝试在单元测试中分配每个 Project 属性 但得到相同的错误结果

我将比 Yoshi 的评论更进一步。

Performed invocations 消息告诉您已调用该方法,但未使用您正在验证的参数。我根据消息的猜测是第一个参数有问题。

你需要post我的测试才能更具体。

更新(添加测试后)

userMgr.Setup 更改为 return 您的 'user' 变量,而不是重复变量。尽管我之前说过,但这就是您失败的原因 - 被测试的代码被重复,并且 Moq 正确地说您的方法没有被调用 user 因为它是被重复调用的.因此,将其更改为此可以解决问题:

userMgr.Setup(x => x.FindByNameAsync(It.IsAny<string>())).ReturnsAsync(user);

如果可以避免使用 It.IsAny<string>(),这可能会变得更加强大:如果将预期作为参数的特定字符串设置为测试设置的一部分,则改为提供该值。

我怀疑两个“1”字符串需要完全相同才能使这项工作正常进行,因此与其复制字符串,不如声明一个局部变量并使用它而不是两个字符串。

我建议永远不要使用像 1 这样的值;更喜欢随机输入一些东西,这样它就不会偶然通过。我的意思是,想象一个采用两个整数作为参数的方法:当为该方法调用 Setup 或 Verify 时,如果您对这两个整数使用相同的值,即使您的代码错误地交换了值,测试也可以通过 (将每个传递给错误的参数)。如果您在调用 Setup 或 Verify 时使用不同的值,则只有在正确的参数中传递正确的值时它才会起作用。

mockRepo.Setup 是多余的。安装程序允许您指定 class 的行为方式,但该行之后没有其他内容,因此它是多余的,可以删除。有些人将设置与 VerifyAll 一起使用,但您可能想阅读有关使用 VerifyAll.

的讨论

现在将您的验证改回使用 project 而不是 It.IsAny<Project>()。我希望它能起作用。

更新 2

考虑一个瓦片屋顶。每块瓦片负责保护屋顶的一小部分,与下面的部分略有重叠。那个瓦片屋顶就像使用模拟时的单元测试集合。

每个'tile'代表一个test fixture,在真实代码中覆盖一个class。 'overlapping' 表示 class 和它使用的东西之间的交互,必须使用模拟来定义,使用诸如设置和验证(在 Moq 中)之类的东西进行测试。

如果这个模拟做得不好,那么瓷砖之间的缝隙就会很大,你的屋顶可能会漏水(即你的代码可能无法正常工作)。两个如何糟糕地进行模拟的例子:

  1. 不检查提供给依赖项的参数,在您确实不需要时使用 It.IsAny
  2. 与真实依赖项的行为方式相比,模拟行为的定义不正确。

最后一个是你最大的风险;但这与编写错误单元测试的风险没有什么不同(无论它是否涉及模拟)。如果我编写了一个单元测试来执行被测代码,但随后未能做出任何断言,或者对无关紧要的事情做出断言,那将是一个弱测试。使用 It.IsAny 就像说 "I don't care what this value is",意味着您错过了断言 应该 的值的机会。

有时无法指定值,您必须使用 It.IsAny,另一种情况我稍后会返回也可以。否则,您应该始终尝试准确指定参数,或者至少使用 It.Is<T>(comparison lambda)。另一次可以使用 It.IsAny<T>() 的情况是当您使用 Times.Never 作为 [=24= 的参数验证调用 而不是 时].在这种情况下,始终使用它通常是个好主意,因为它会检查是否没有使用任何参数进行调用(避免您只是在给定的参数上犯了错误)。

如果我写了一些单元测试,代码覆盖率达到 100%;但没有测试所有可能的场景,那将是薄弱的单元测试。我是否有任何测试来尝试找出这些写得不好的测试?不,不使用模拟的人也没有这样的测试。

回到瓦屋顶类比...如果我没有模拟,并且必须使用真正的依赖项测试每个部分,这就是我的屋顶的样子。我可以为屋顶底部边缘的所有钻头铺上瓷砖。到目前为止没问题。对于屋顶上的下一组瓦片,对于本来是一块瓦片的东西,我需要一个三角形的瓦片,覆盖那块瓦片应该去的地方,并覆盖它下面的瓦片(即使它们已经被一块瓷砖)。不过,还不错。但是 15继续往屋顶走,这会让人筋疲力尽的。

将其带到现实世界的场景中,假设我正在测试一段 client-side 代码,它使用两个 WCF 服务,其中一个是按使用收费的第三方,其中一个是受到 windows 身份验证的保护,也许其中一个服务在到达数据层并与数据库交互之前在其业务层中具有复杂的逻辑,并且在其中的某个地方,我可能有一些缓存。我敢说在没有模拟的情况下为此编写体面的测试可以描述为 overly-convoluted,如果它甚至可能(在一个人的一生中)...

除非你使用模拟,它允许你...

  1. 测试依赖于 third-party 代码的代码,而不调用它(承认前面提到的关于准确模拟该代码的风险)。
  2. 模拟如果有或没有正确权限的用户调用受保护的 WCF 服务会发生什么(考虑如何在没有模拟的情况下从自动化测试中做到这一点)
  3. 单独测试代码的不同部分,这在涉及复杂业务逻辑时特别有价值。这会以指数方式减少通过需要测试的代码的路径数量,从而降低编写测试和维护测试的成本。想象一下必须设置具有所有先决条件的数据库的复杂性,不仅针对数据层测试,而且针对调用堆栈的所有测试。现在当数据库发生变化时会发生什么?
  4. 通过验证调用模拟方法的次数来测试缓存。

(郑重声明,测试的执行速度从未影响我使用模拟的决定。)

幸运的是,模拟很简单,只需要比我在这里阐述的内容更高的理解水平。只要您承认使用模拟与 full-on 集成测试相比是一种妥协,它会节省开发和维护时间,任何产品经理都会感激不已。所以尽量保持瓷砖之间的间隙小。

尝试像这样设置你的方法:

mockRepo.Setup(m => m.SaveProject(It.IsAny(),It.IsAny())

然后也使用 It.IsAny 进行验证。

或者只使用 It.IsAny 用于您出于某种原因不想(或不能)正确检查的参数。您还可以在后一种情况下创建自定义匹配器。

如其他评论所述。问题很可能出在您模拟期望的设置参数上。