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

使用Moq进行C# EF Core单元测试:AddAsync模拟返回Null问题

问题

在为CQRS命令搭建基于Moq的C# EF Core单元测试时,命令处理程序可正常运行,但单元测试中模拟AddAsync调用后,RaiseDomainEvent函数里的newUser始终为null。


用户/用户设置实体(领域建模)

public class User
{
    private User(int id, string username)
    {
        UserId = id;
        UserName = username;
    }
    public int UserId {get; private set;}
    public string UserName {get; private set;}
    public List<UserSetting> UserSettings {get; private set;}

    public static User Create(int id, string username)
    {
        return new User(id, username);
    }
    public static DomainEvent RaiseDomainEvent(User user, List<UserSetting> settings)
    {
        // Raise the event and handle it
    }
}

public class UserSetting
{
    public int SettingId {get; set;}
    public int UserId {get; set;}
    public int SettingTypeId {get; set;}
    public string SettingValue {get; set;}
}

添加用户命令

public sealed record AddUserCommand (
    int UserId, string UserName, List<UserSetting> UserSettings
) : ICommand; 

添加用户命令处理程序(基于Mediatr)

internal sealed class AddUserCommandHandler : ICommandHandler<AddUserCommand>
{
    private readonly IMapper _mapper;
    private readonly IUserRepo _userRepo;

    public AddUserCommandHandler(IMapper mapper, IUserRepo userRepo)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
        _userRepo = userRepo ?? throw new ArgumentNullException(nameof(userRepo));
    }

    public async Task<Result> Handle(AddUserCommand cmd, CancellationToken ct = default)
    {
        User dbUser = await _userRepo.GetUserByIdAsync(cmd.UserId, ct);

        if (dbUser != null) throw new Exception($"User exists with id: {cmd.UserId}");

        User newUser = User.Create(cmd.UserId, cmd.UserName);

        // 由于ID在数据库添加时生成,需要获取它来添加FK关系以关联设置
        newUser = await _userRepo.AddAsync(newUser, ct);

        if (cmd.UserSettings != null && cmd.UserSettings.Any())
        {
            User.RaiseDomainEvent(newUser, cmd.UserSettings);
        }

        return new Result 
        {
            Success = true
        };
    }
}

AddAsync实现

public async Task<User> AddAsync(User entity, CancellationToken ct = default)
{
    _context.Users.Add(entity);
    await _context.SaveChangesAsync(ct);

    return entity;
}

单元测试代码

public class AddUserCommandHandlerTests
{
    private readonly Mock<IMapper> _mapperMock;
    private readonly Mock<IUserRepo> _userRepoMock;

    public AddUserCommandHandlerTests()
    {
        _mapperMock = new Mock<IMapper>();
        _userRepoMock = new Mock<IUserRepo>();    
    }

    // 该测试正常运行
    [Fact]
    public async Task Handle_Should_Return_SuccessResult_NoSettings()
    {
        AddUserCommand cmd = new AddUserCommand(0, "stacktest", new List<UserSetting>());

        User mockDbUser = null;

        _userRepoMock.Setup(x => x.GetUserByIdAsync(cmd.UserId, default).Result).Returns(mockDbUser);

        var handler = new AddUserCommandHandler(_mapperMock.Object, _userRepoMock.Object);

        var result = await handler.Handle(cmd, default);

        Assert.True(result.Success);
    }

    // 该测试失败,提示在User.RaiseDomainEvent(newUser, cmd.UserSettings)处newUser为null
    [Fact]
    public async Task Handle_Should_Return_SuccessResult_WithSettings()
    {
        AddUserCommand cmd = new AddUserCommand(0, "stacktest", new List<UserSetting> { new UserSetting{ SettingId = 0, SettingTypeId = 1, UserId = 0, SettingValue = "Test"}});

        User mockDbUser = null;

        User addedDbUser = User.Create(10, cmd.UserName);

        _userRepoMock.Setup(x => x.GetUserByIdAsync(cmd.UserId, default).Result).Returns(mockDbUser);
        _userRepoMock.Setup(x => x.AddAsync(mockDbUser, default).Result).Returns(addedDbUser);

        var handler = new AddUserCommandHandler(_mapperMock.Object, _userRepoMock.Object);

        var result = await handler.Handle(cmd, default);

        Assert.True(result.Success);
    }
}

问题原因与解决办法

核心原因

测试中模拟AddAsync时,传入的参数是mockDbUser(值为null),但实际命令处理程序调用AddAsync时传入的是通过User.Create创建的newUser实例。Moq采用严格参数匹配规则,这个模拟设置不会被触发,导致AddAsync返回默认值null,最终newUser变为null。

解决步骤

  1. 修正AddAsync的参数匹配规则
    不要用具体的mockDbUser作为匹配参数,改用It.IsAny<User>()匹配任意User实例,或者提前创建和命令处理程序中一致的newUser进行精确匹配:

    // 方式1:匹配任意User实例
    _userRepoMock.Setup(x => x.AddAsync(It.IsAny<User>(), default).Result).Returns(addedDbUser);
    
    // 方式2:精确匹配目标实例
    User expectedNewUser = User.Create(cmd.UserId, cmd.UserName);
    _userRepoMock.Setup(x => x.AddAsync(expectedNewUser, default).Result).Returns(addedDbUser);
    
  2. 优化异步方法模拟方式
    直接访问.Result模拟异步方法不是最佳实践,建议用ReturnsAsync更自然地模拟异步返回:

    _userRepoMock.Setup(x => x.GetUserByIdAsync(cmd.UserId, default)).ReturnsAsync(mockDbUser);
    _userRepoMock.Setup(x => x.AddAsync(It.IsAny<User>(), default)).ReturnsAsync(addedDbUser);
    
  3. 完整修正后的测试代码

    [Fact]
    public async Task Handle_Should_Return_SuccessResult_WithSettings()
    {
        AddUserCommand cmd = new AddUserCommand(0, "stacktest", new List<UserSetting> { new UserSetting{ SettingId = 0, SettingTypeId = 1, UserId = 0, SettingValue = "Test"}});
    
        User mockDbUser = null;
        User addedDbUser = User.Create(10, cmd.UserName);
    
        _userRepoMock.Setup(x => x.GetUserByIdAsync(cmd.UserId, default)).ReturnsAsync(mockDbUser);
        _userRepoMock.Setup(x => x.AddAsync(It.IsAny<User>(), default)).ReturnsAsync(addedDbUser);
    
        var handler = new AddUserCommandHandler(_mapperMock.Object, _userRepoMock.Object);
        var result = await handler.Handle(cmd, default);
    
        Assert.True(result.Success);
    }
    

内容的提问来源于Stack Exchange,提问作者Travis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 06:05:24