模拟验证()调用

时间:2023-02-27
本文介绍了模拟验证()调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

我正在单元测试,看看是否调用了一个方法.

I'm unit testing to see if a method is called.

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

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

While debugging, I can verify that the method is actually called in the controller,

//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)      

调试实际上并不显示进入存储库方法.确切的错误如下

Debugging doesn't actually show entrance into the repository method. The exact error is below

预期至少对模拟调用一次,但从未执行过:m => m.SaveProject(, JohnDoe)

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

未配置任何设置.执行的调用:IRepository.ProjectClassIRepository.SaveProjects(ProjectClass, JohnDoe)'

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

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

When I do an actual integration test, the SaveProject method is touched in the repository and seems to work properly. I've also tried assigning every Project property within the unit test but got the same error result

推荐答案

我会比 Yoshi 的评论更进一步.

I'm going to go a step further than Yoshi's comment.

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

The Performed invocations message tells you the method was called but not with the parameters that you were verifying. My guess based on the messages is that there's something wrong with the first parameter.

您需要发布测试让我能够更具体.

You would need to post the test for me to be able to be more specific.

更新(添加测试后)

更改 userMgr.Setup 以返回您的用户"变量,而不是重复.尽管我之前说过,这是您失败的原因 - 正在测试的代码被重复,并且 Moq 正确地说您的方法没有被 user 调用,因为它已被调用与副本.所以把它改成这个可以解决问题:

Change userMgr.Setup to return your 'user' variable, not a duplicate. Despite what I said earlier, this was the cause of your failure - the code being tested was being given a duplicate, and Moq was correctly saying that your method had not been called with user because it had been called with the duplicate. So changing it to this fixes the problem:

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

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

This could be made even stronger if the use of It.IsAny<string>() can be avoided: if the specific string that is expected as a parameter is set up as part of the test setup, then give the value instead.

我怀疑两个1"字符串需要相同才能使这项工作正常工作,因此不要复制字符串,而是声明一个局部变量并使用它而不是两个字符串.

I suspect both of the "1" strings need to be identical to make this work, so rather than duplicate the string declare a local variable and use that instead of both strings.

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

I would suggest never using values like 1; prefer to randomly type something, so that it doesn't coincidentally pass. By which I mean, imagine a method which takes two integers as parameters: when calling Setup or Verify for that method, if you use the same value for both those integers, the test could pass even if your code has mistakenly swapped the values over (passing each into the wrong parameter). If you use different values when calling Setup or Verify, then it will only work when the correct value is passed in the correct parameter.

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

mockRepo.Setup is redundant. Setup allows you to specify how the class behaves but there is nothing else after that on the line, so its redundant and can be removed. Some people use setup along with VerifyAll but you might want to read this discussion about using VerifyAll.

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

Now change your verify back to using project rather than It.IsAny<Project>(). I would expect it to work.

更新 2

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

Consider a tiled roof. Each tile is responsible for protecting one small part of the roof, slightly overlapping the ones below it. That tiled roof is like a collection of unit tests when using mocking.

每个 'tile' 代表一个测试夹具,覆盖真实代码中的一个类.重叠"表示类与其使用的事物之间的交互,必须使用模拟定义,模拟使用诸如设置和验证(在 Moq 中)之类的东西进行测试.

Each 'tile' represents one test fixture, covering one class in the real code. The 'overlapping' represents the interaction between the class and the things it uses, which has to be defined using mocks, which are tested using things like Setup and Verify (in Moq).

如果这个模拟做得不好,那么瓷砖之间的间隙会很大,并且您的屋顶可能会泄漏(即您的代码可能无法正常工作).模拟如何做得不好的两个例子:

If this mocking is done badly, then the gaps between the tiles will be big, and your roof could leak (i.e. your code might not work). Two examples of how mocking can be done badly:

  1. 当你真的不需要时,通过使用 It.IsAny 不检查提供给依赖项的参数.
  2. 与实际依赖项的行为方式相比,mock 行为的定义不正确.
  1. Not checking the parameters which are given to the dependencies, by using It.IsAny when you really don't need to.
  2. Incorrectly defining the behaviour of the mock compared to how the real dependency would behave.

最后一个是你最大的风险;但这与编写糟糕的单元测试的风险没有什么不同(不管它是否涉及模拟).如果我编写了一个单元测试,它执行了被测代码,但没有做出任何断言,或者对无关紧要的东西做出断言,那将是一个弱测试.使用 It.IsAny 就像在说我不在乎这个值是什么",这意味着您错过了断言该值应该是什么的机会.

That last one is your biggest risk; but it's no different than the risk of writing bad unit tests (regardless of whether it involves mocking). If I wrote a unit test which exercised the code under test but then failed to make any assertions, or made an assertion about something that doesn't matter, that would be a weak test. Using It.IsAny is like saying "I don't care what this value is", and means you're missing the opportunity to assert what that value should be.

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

There are times when it's not possible to specify the value, where you have to use It.IsAny, and one other case I'll come back to in a second is also OK. Otherwise, you should always try to specify what the parameters are, either exactly, or at least using It.Is<T>(comparison lambda). The one other time it's ok to use It.IsAny<T>() is when you are verifying that a call has not been made, using Times.Never as a parameter to Verify. In this case, it is usually a good idea to always use it, since it checks the call has not been made with any parameter (avoiding the possibility that you have simply made an error on what parameters are given).

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

If I wrote some unit tests which gave me 100% code coverage; but didn't test all the possible scenarios, that would be weak unit testing. Do I have any tests to try to find these badly written tests? No, and people who don't use mocking don't have tests like that either.

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

Going back to the tiled roof analogy... if I didn't have mocking, and had to test each part using the real dependencies here's what my roof would look like. I could have a tile for all of the bits at the bottom edge of the roof. No problem so far. For what would have been the next set of tiles up the roof, for what would have been one tile, I need a triangular tile, covering where that tile would have gone, and covering the tiles below it (even though they are already covered by a tile). Still, not too bad. But 15 tiles further up the roof, this is going to get exhausting.

将其带到现实世界的场景中,假设我正在测试一段客户端代码,该代码使用两个 WCF 服务,其中一个是按使用收费的第三方,其中一个受 Windows 身份验证保护,也许其中一个服务在到达数据层并与数据库交互之前在其业务层中具有复杂的逻辑,并且在那里的某个地方,我可能有一些缓存.我敢说为此编写体面的测试而不用嘲笑可以被描述为过于复杂,如果它甚至可能的话(在一个人的一生中)......

Bringing that to a real world scenario, imagine I'm testing a client-side piece of code, which uses two WCF services, one of which is a third party that charges per use, one of which is protected by windows authentication, maybe one of those services has complex logic in its business layer before reaching the data layer and interacting with a database, and somewhere in there, I might have some caching. I daresay writing decent tests for this without mocking could be described as overly-convoluted, if it's even possible (in one person's lifetime)...

除非您使用模拟,否则您可以...

Unless you use mocking, which allows you to...

  1. 在不调用第三方代码的情况下测试您的代码(承认前面提到的关于准确模拟的风险).
  2. 模拟如果有或没有正确权限的用户调用受保护的 WCF 服务会发生什么(想想如何在没有模拟的情况下从自动化测试中做到这一点)
  3. 单独测试代码的各个部分,这在涉及复杂业务逻辑的情况下尤其有用.这成倍地减少了需要测试的代码路径数量,降低了编写测试和维护测试的成本.想象一下,必须设置具有所有先决条件的数据库的复杂性,不仅针对数据层测试,而且针对调用堆栈上的所有测试.现在,当发生数据库更改时会发生什么?
  4. 通过验证您的模拟方法被调用的次数来测试缓存.

(为了记录,测试的执行速度对我使用模拟的决定没有任何影响.)

(For the record, speed of execution of the tests has never played any part in my decision to use mocking.)

幸运的是,嘲弄很简单,几乎不需要任何程度的理解就可以超出我在这里所阐述的内容.只要您承认与全面集成测试相比,使用模拟是一种折衷方案,它就可以节省开发和维护时间,任何产品经理都会感激不尽.所以尽量保持瓷砖之间的间隙很小.

Luckily mocking is simple, requiring barely any level of comprehension above what I have spelled out here. As long as you acknowledge that using mocking is a compromise compared to full-on integration testing, it yields the kind of savings in development and maintenance time that any product manager will be grateful for. So try to keep the gaps between your tiles small.

这篇关于模拟验证()调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!

上一篇:如何使用 MOQ 框架在 c# 中模拟静态方法? 下一篇:使用 Moq 确定是否调用了方法

相关文章

最新文章