You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Moq验证失败:已调用AttemptToCreateUser但单元测试不通过

问题:Moq验证方法调用失败,但已执行调用记录显示方法已被调用

我正在测试注册接口的RegisterUser方法,核心目标是验证this.userService.AttemptToCreateUser(newUser, registerDto.Password)是否被调用。但测试执行后失败,错误信息显示预期调用1次但实际0次,可“已执行调用”部分明明显示该方法用相同参数被调用过了。


注册接口实现代码

[HttpPost("Register")]
public async Task<ActionResult<UserDto>> RegisterUser([FromBody] RegisterDto registerDto)
{
    _logger.Info($"Register user end point hit. Username {registerDto.Username}");

    await validationService.Validate(registerDto);

    if (!await this.userService.CheckUserDoesNotAlreadyExistByName(registerDto.Username))
    {
        throw new ValidationException("Username already exists");
    }
    
    if(!await this.userService.CheckUserDoesNotAlreadyExistByEmail(registerDto.Email))
    {
        throw new ValidationException("Email already exists");
    }

    _logger.Info($"Register dto model validation complete. Attempting to create user. Username: {registerDto.Username}. Email: {registerDto.Email}");

    var newUser = this.mapper.Map<AppUser>(registerDto);

    var result = await this.userService.AttemptToCreateUser(newUser, registerDto.Password);

    if (!result.Succeeded)
    {
        AggregateException aggregateException = this.userService.HandleUnsuccessfulUserCreation(result.Errors);
        throw aggregateException;
    }
    else
    {
        return Ok(await this.userService.HandleSuccessfulUserCreation(newUser));
    }
}            

单元测试代码

[Fact]
public async void RegisterUser_should_call_user_service_AttemptToCreateUser_method()
{
    //arrange 
    var registerDto = MockRegisterDtoGenerator.GenerateMockRegisterDto();
    var appUser = MockUserGenerator.GenerateMockUser();
    var mockUserService = MockUserServiceGenerator.GenerateMockUserService(appUser);
    var sut = AccountControllerGenerator.CreateAccountController(mockUserService.Object);

    //Act 
    await sut.RegisterUser(registerDto);

    //assert
    mockUserService.Verify(x => x.AttemptToCreateUser(appUser, registerDto.Password), Times.Once);
}

MockUserService生成方法

internal static Mock<IUserService> GenerateMockUserService(AppUser mockUser)
{
    var userTask = Task.FromResult(mockUser);
    var mockUserService = new Mock<IUserService>();
    var identityResult = IdentityResult.Success;
    var identityResultTask = Task.FromResult(identityResult);
    mockUserService.Setup(x => x.UpdateUser(It.IsAny<AppUser>())).Returns(identityResultTask);
    mockUserService.Setup(x => x.AttemptToCreateUser(It.IsAny<AppUser>(), It.IsAny<string>())).Returns(identityResultTask);
    mockUserService.Setup(x => x.FindByIdAsync(It.IsAny<int>())).Returns(userTask);
    mockUserService.Setup(x => x.CheckUserDoesNotAlreadyExistByEmail(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true));
    mockUserService.Setup(x => x.CheckUserDoesNotAlreadyExistByName(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true));
    return mockUserService;
}

测试错误信息

Moq.MockException : Expected invocation on the mock once, but was 0 times: x => x.AttemptToCreateUser(Mr Test, "Pa$$w0rd")

Configured setups: x => x.AttemptToCreateUser(It.IsAny(), It.IsAny()), Times.Once

Performed invocations:

IUserService.CheckUserDoesNotAlreadyExistByName("Mr Test", System.Threading.CancellationToken)

IUserService.CheckUserDoesNotAlreadyExistByEmail("TestEmail@gmail.com", System.Threading.CancellationToken)

IUserService.AttemptToCreateUser(Mr Test, "Pa$$w0rd")

IUserService.HandleSuccessfulUserCreation(Mr Test)


原因分析

问题出在Moq的参数匹配逻辑上:
你在验证时传入的appUser是测试代码里MockUserGenerator.GenerateMockUser()创建的实例,但控制器中调用this.mapper.Map<AppUser>(registerDto)生成的newUser是另一个全新的AppUser对象。虽然两个对象的属性(用户名、邮箱等)完全一致,但它们是内存中两个不同的引用实例。

Moq默认使用引用相等来匹配对象参数,所以它认为验证时的appUser和实际调用时的newUser不是同一个参数,因此判定“未调用”。

解决方案

有两种常用的解决方式:

方式1:基于属性值匹配参数

修改验证逻辑,不直接用引用对象,而是通过属性值来匹配AppUser参数:

mockUserService.Verify(x => x.AttemptToCreateUser(
    It.Is<AppUser>(u => u.Username == appUser.Username && u.Email == appUser.Email),
    registerDto.Password), 
Times.Once);

这种方式只关心对象的属性是否符合预期,不要求是同一个引用实例。

方式2:让Mapper返回测试用的appUser实例

如果需要严格验证确实是某个特定实例被传入,可以Mock控制器依赖的IMapper,让它在调用Map<AppUser>(registerDto)时返回你测试中创建的appUser:

  1. 在测试的Arrange阶段添加Mock的Mapper:
var mockMapper = new Mock<IMapper>();
mockMapper.Setup(m => m.Map<AppUser>(registerDto)).Returns(appUser);
  1. 创建控制器时把这个Mock的Mapper传入(需要修改AccountControllerGenerator.CreateAccountController方法,支持传入Mapper参数)。

这样控制器中生成的newUser就是测试里的appUser实例,Moq的引用匹配就能通过。


内容的提问来源于stack exchange,提问作者Pablo52

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 06:44:59