使用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。
解决步骤
修正
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);优化异步方法模拟方式
直接访问.Result模拟异步方法不是最佳实践,建议用ReturnsAsync更自然地模拟异步返回:_userRepoMock.Setup(x => x.GetUserByIdAsync(cmd.UserId, default)).ReturnsAsync(mockDbUser); _userRepoMock.Setup(x => x.AddAsync(It.IsAny<User>(), default)).ReturnsAsync(addedDbUser);完整修正后的测试代码
[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
相关产品推荐
相关产品推荐

