如何用Moq模拟ASP.NET Identity登录失败并编写单元测试?
解决ASP.NET MVC 5登录失败场景的单元测试问题
你现在遇到的核心问题是没有给SignInManager的Mock配置PasswordSignInAsync方法的返回值,Moq对于异步方法会默认返回已完成的Task,而SignInStatus枚举的默认值是Success,所以你的测试里总是走到成功分支。下面一步步调整:
1. 调整测试类成员,暴露SignInManager的Mock
首先在测试类中新增一个类级别的Mock成员,方便后续在测试方法中配置它的行为:
private Mock<ApplicationSignInManager> _signInManagerMock; // 新增该成员
2. 修改SetContexts方法,绑定类级别的Mock
把SetContexts里创建signInManager的局部变量替换为类成员,确保测试方法能访问到这个Mock:
// 替换原来的局部signInManager变量 _signInManagerMock = new Mock<ApplicationSignInManager>(userManager.Object, authenticationManager.Object); // 后续传入AccountController的代码保持不变 _controller = new AccountController( userManager.Object, _signInManagerMock.Object) { ControllerContext = new ControllerContext() { HttpContext = _httpContextMock.Object } };
3. 在测试方法中配置登录失败的返回逻辑
在测试方法的Arrange阶段,明确设置PasswordSignInAsync返回SignInStatus.Failure,同时调整断言逻辑对应失败场景的预期:
[TestMethod] public async Task Login_ReturnsViewWithModelError_WhenLoginFails() { // Arrange var claimsIdentity = new ClaimsIdentity(); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, "fake@fake.org")); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); SetContexts(claimsPrincipal); var vm = new LoginViewModel { Email = "faker@gmail.com", Password = "Faker9999!", RememberMe = false }; // 关键配置:让SignInManager返回登录失败 _signInManagerMock.Setup(s => s.PasswordSignInAsync( vm.Email, vm.Password, vm.RememberMe, It.IsAny<bool>())) // 匹配shouldLockout参数的任意值 .ReturnsAsync(SignInStatus.Failure); // Act var result = await _controller.Login(vm, "http://www.google.com") as ViewResult; // Assert // 断言返回结果是ViewResult而非重定向 Assert.IsNotNull(result, "登录失败时应返回ViewResult"); // 断言ModelState标记为无效 Assert.IsFalse(_controller.ModelState.IsValid, "登录失败后ModelState应无效"); // 断言存在对应的错误消息 Assert.IsTrue(_controller.ModelState.ContainsKey(string.Empty), "ModelState应包含全局错误"); Assert.AreEqual("Invalid login attempt.", _controller.ModelState[string.Empty].Errors[0].ErrorMessage); // 断言返回的视图模型与传入的一致 Assert.AreEqual(vm, result.Model, "返回的视图模型应与输入一致"); }
补充说明
- 我把测试方法名改成了更具语义的
Login_ReturnsViewWithModelError_WhenLoginFails,这样能直接看出测试的场景和预期结果。 - 如果不需要匹配特定参数,也可以用
It.IsAny<string>()、It.IsAny<bool>()来兼容任意输入,适合测试通用的登录失败场景。 - 登录失败时控制器的核心行为是返回原视图并添加错误信息,所以断言要围绕ViewResult返回、ModelState状态、错误内容这几个核心点展开。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

