问题
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
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)'
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
回答1:
I'm going to go a step further than Yoshi's comment.
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.
Update (after the Test was added)
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);
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.
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.
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
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.
Now change your verify back to using project
rather than It.IsAny<Project>()
. I would expect it to work.
Update 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.
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:
- Not checking the parameters which are given to the dependencies, by using
It.IsAny
when you really don't need to. - Incorrectly defining the behaviour of the mock compared to how the real dependency would behave.
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.
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).
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.
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.
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...
- Test your code that depends on the third-party code, without making calls into it (acknowledging the risks mentioned earlier about mocking that accurately).
- Simulate what would happened if a user with or without the right permissions called the protected WCF service (think about how you would do that from automated tests without mocking)
- Test separate parts of code in isolation, which is particularly valuable where complex business logic is involved. This exponentially reduces the number of paths through the code that need to be tested, reducing the cost of writing the tests, and of maintaining them. Imagine the complexity of having to set up the database with all the prerequisites, not just for the data layer tests, but for all of the tests up the call stack. Now what happens when there is a database change?
- Test caching by Verifying how many times your mock's method was called.
(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.
回答2:
Try to setup your method like this:
mockRepo.Setup(m => m.SaveProject(It.IsAny(),It.IsAny())
And then verify using It.IsAny as well.
Or just use It.IsAny for the parameters you do not want (or cannot) check properly for some reason. You can also create custom matchers in the later case.
As mentioned in other comments. The problem is likely to be on the arguments that you have setup you mock to expect.
来源:https://stackoverflow.com/questions/49332471/mock-verify-invocation