使用Moq模拟返回Guid的方法时单元测试失败问题排查
问题:Clean架构应用层测试失败,CreateAsync返回空Guid而非预期值
在Clean架构应用层编写测试用例时,遇到测试失败:_movieRepository.CreateAsync()方法未返回预期Guid值CF0A8C1C-F2D0-41A1-A12C-53D9BE513A1C,而是返回空Guid00000000-0000-0000-0000-000000000000。
Fixture类代码
public class MovieFixture { private readonly Mock<IMovieRepository> _movieRepository = new Mock<IMovieRepository>(); private ServiceCollection ServiceCollection { get; } = new ServiceCollection(); public CreateMovieCommandHandler CreateMovieCommandHandler { get { ServiceProvider serviceProvider = ServiceCollection.BuildServiceProvider(); CreateMovieCommandHandler service = serviceProvider.GetRequiredService<CreateMovieCommandHandler>(); return service; } } public MovieFixture() { var sampleGuid = new Guid("{CF0A8C1C-F2D0-41A1-A12C-53D9BE513A1C}"); #region Movie command handler mocks // Create movie. _movieRepository .Setup(x => x.CreateAsync(_successMovie.ToMovieDomainModel())) .ReturnsAsync(sampleGuid); _movieRepository .Setup(x => x.CreateAsync(_failedMovie)) .ReturnsAsync(Guid.Empty); #endregion #region Add transient services AddServices(); #endregion } private void AddServices() { // Add repository mock _ = ServiceCollection.AddTransient(_ => _movieRepository.Object); // Add handlers _ = ServiceCollection.AddTransient<CreateMovieCommandHandler>(); // Add validator _ = ServiceCollection.AddTransient<MovieValidator<CreateMovieCommand>>(); } private CreateMovieCommand _successMovie = new CreateMovieCommand { Name = "success", Description = "test", IsActive = true, IsExpired = false, Image = "some-image.png", StartDate = DateTime.Now }; private Movie _failedMovie = new Movie { Name = "failed", Description = "test", IsActive = true, IsExpired = false, Image = "some-image.png", StartDate = DateTime.Now }; }
失败的测试用例代码
[Fact] public async Task Handle_WhenRepositoryReturnsSuccess_ShouldReturnUnitValue() { // Arrange Exception? exception = default; var handler = _movieFixture.CreateMovieCommandHandler; Unit result = default; // Act try { result = await handler.Handle(_successMovie, CancellationToken.None); } catch (Exception ex) { exception = ex; } //Assert exception.Should().BeNull(); result.Should().Be(Unit.Value); }
命令处理器代码
public async Task<Unit> Handle(CreateMovieCommand request, CancellationToken cancellationToken) { var validationResult = await _validator.ValidateAsync(request).ConfigureAwait(false); if (validationResult.IsValid is false) throw new ValidationException(validationResult.Errors.FirstOrDefault()?.ToString()); var result = await _movieRepository.CreateAsync(request.ToMovieDomainModel()).ConfigureAwait(false); if (result == Guid.Empty) throw new MovieCannotBeAddedException(ErrorMessages.MovieCannotBeAdded); return Unit.Value; }
问题原因
Moq默认采用引用匹配规则:Fixture中Setup时传入的是_successMovie.ToMovieDomainModel()生成的具体对象实例,而测试运行时,命令处理器里调用request.ToMovieDomainModel()会生成新的对象实例。这两个实例属性值相同但引用不同,导致Moq无法匹配到对应的Setup,最终返回默认值空Guid。
解决方案
方案1:使用It.Is<T>进行属性匹配
修改Fixture中的Setup逻辑,通过属性值匹配而非引用匹配,确保只要传入的Movie对象属性符合预期,就触发对应的返回值:
_movieRepository .Setup(x => x.CreateAsync(It.Is<Movie>(m => m.Name == _successMovie.Name && m.Description == _successMovie.Description && m.IsActive == _successMovie.IsActive && m.IsExpired == _successMovie.IsExpired && m.Image == _successMovie.Image && // 注意:DateTime.Now会导致时间匹配失败,改为固定值或忽略细微差异 m.StartDate.Date == _successMovie.StartDate.Date))) .ReturnsAsync(sampleGuid);
提示:Fixture中的
_successMovie.StartDate不要用DateTime.Now,改为固定值(如new DateTime(2024, 1, 1)),避免时间差异导致匹配失败。
方案2:重写Movie类的Equals和GetHashCode方法
如果Movie是自定义领域模型,重写Equals和GetHashCode方法,让Moq可以通过属性值判断两个对象相等:
public class Movie { // 原有属性... public override bool Equals(object? obj) { if (obj is not Movie other) return false; return Name == other.Name && Description == other.Description && IsActive == other.IsActive && IsExpired == other.IsExpired && Image == other.Image && StartDate == other.StartDate; } public override int GetHashCode() { return HashCode.Combine(Name, Description, IsActive, IsExpired, Image, StartDate); } }
方案3:复用领域模型实例
在Fixture中提前生成并保存_successMovie对应的领域模型实例,Setup和测试流程中复用该实例:
public class MovieFixture { private Movie _successMovieDomain; private CreateMovieCommand _successMovie = new CreateMovieCommand { Name = "success", Description = "test", IsActive = true, IsExpired = false, Image = "some-image.png", StartDate = new DateTime(2024, 1, 1) // 固定时间 }; public MovieFixture() { var sampleGuid = new Guid("{CF0A8C1C-F2D0-41A1-A12C-53D9BE513A1C}"); _successMovieDomain = _successMovie.ToMovieDomainModel(); _movieRepository .Setup(x => x.CreateAsync(_successMovieDomain)) .ReturnsAsync(sampleGuid); // ...其他逻辑 } }
内容的提问来源于stack exchange,提问作者kebechet_200
相关产品推荐
相关产品推荐

