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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 15:05:01