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

使用Xunit和Moq编写.NET Core中AgeCategoryRequestHandler单元测试的困惑

.NET Core中AgeCategoryRequestHandler的单元测试指导(Xunit+Moq)

现有测试代码的合理性与问题

合理之处

  • 针对构造函数依赖(IRepositoryResolver、ILogger)创建Mock对象,符合依赖注入的测试原则,避免耦合真实数据库或外部服务
  • 使用AutoFixture自动生成测试数据,减少手动构造对象的重复工作
  • 尝试验证仓储方法的调用次数,抓住了核心逻辑的测试点

存在的问题

  1. 错误Mock被测类:代码中Mock<AgeCategoryRequestHandler>完全多余,我们要测试的是真实的类实例,而非Mock它
  2. 异步测试方法错误:async void在Xunit中会导致测试不等待异步操作完成,必须改为async Task
  3. 测试场景模糊:第一个测试方法的断言逻辑不对应明确的业务场景,无法验证核心业务规则
  4. 缺失关键Mock定义:第二个代码片段中_mockRepository未定义,需要补充对应仓储接口(比如IEstabAgeCategoryRepository)的Mock
  5. 未覆盖异常分支:没有测试仓储解析失败(返回null)的场景
  6. 未验证日志输出:原代码中有日志记录,测试中未验证日志是否正确触发

完整测试步骤与方法

步骤1:准备依赖Mock

需要Mock三个核心依赖:

  • IRepositoryResolver:控制仓储的解析结果
  • ILogger:验证日志输出
  • 仓储接口(假设为IEstabAgeCategoryRepository):模拟数据操作的返回结果

步骤2:初始化被测实例

用Mock的依赖对象创建真实的AgeCategoryRequestHandler实例(即被测系统SUT),不要Mock被测类本身。

步骤3:构造目标测试数据

用AutoFixture或手动创建InventoryRequest,精准控制EstabAgeCategories的数量,覆盖不同业务场景。

步骤4:设置Mock行为

根据测试场景,定义Mock对象的返回值:

  • 正常场景:让ResolveEstabAgeCategory返回Mock仓储,仓储的InsertUpdate方法返回指定的处理数量
  • 异常场景:让ResolveEstabAgeCategory返回null,模拟仓储找不到的情况

步骤5:执行被测方法

调用Run方法(完整流程)或直接调用CreateResponse(单独测试响应逻辑)。

步骤6:验证断言

  • 验证Mock方法的调用次数(比如仓储方法是否被调用、调用次数是否正确)
  • 验证返回的HandlerResult属性是否符合业务规则
  • 验证日志是否按预期输出

核心测试场景与示例代码

场景1:所有数据处理成功(结果数=请求数)

[Fact]
public async Task Run_AllRecordsProcessed_ReturnsSuccess()
{
    // Arrange
    var fixture = new Fixture();
    var requestCount = 3;
    var inventoryRequest = fixture.Build<InventoryRequest>()
        .With(x => x.EstabAgeCategories, fixture.CreateMany<EstabAgeCategory>(requestCount).ToList())
        .Create();

    _mockAgeCategoryRepo.Setup(r => r.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories))
        .ReturnsAsync(requestCount);

    // Act
    var result = await _sut.Run(inventoryRequest);

    // Assert
    _mockAgeCategoryRepo.Verify(r => r.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories), Times.Once);
    Assert.Equal(RequestHandlerEnum.AgeCategrory.ToInt(), result.Id);
    Assert.True(result.IsSuccess);
    Assert.Equal(0, result.ErrorCode);
    _mockLogger.Verify(l => l.Info("AgeCategory requesthandler processrequest start"), Times.Once);
    _mockLogger.Verify(l => l.Info("AgeCategory requesthandler processrequest complete"), Times.Once);
}

场景2:部分数据处理失败(结果数<请求数)

[Fact]
public async Task Run_PartialRecordsProcessed_ReturnsFailure()
{
    // Arrange
    var fixture = new Fixture();
    var requestCount = 5;
    var processedCount = 3;
    var inventoryRequest = fixture.Build<InventoryRequest>()
        .With(x => x.EstabAgeCategories, fixture.CreateMany<EstabAgeCategory>(requestCount).ToList())
        .Create();

    _mockAgeCategoryRepo.Setup(r => r.InsertUpdateEstabAgeCategoryDetail(inventoryRequest.EstabAgeCategories))
        .ReturnsAsync(processedCount);

    // Act
    var result = await _sut.Run(inventoryRequest);

    // Assert
    Assert.False(result.IsSuccess);
    Assert.Equal(OTAErrorType.InvalidAgeCategory.ToInt(), result.ErrorCode);
}

场景3:仓储解析失败(返回null)

[Fact]
public async Task Run_RepositoryNotFound_ReturnsError()
{
    // Arrange
    var fixture = new Fixture();
    var inventoryRequest = fixture.Build<InventoryRequest>()
        .With(x => x.EstabAgeCategories, fixture.CreateMany<EstabAgeCategory>(2).ToList())
        .Create();

    _mockRepoResolver.Setup(r => r.ResolveEstabAgeCategory()).Returns((IEstabAgeCategoryRepository)null);

    // Act
    var result = await _sut.Run(inventoryRequest);

    // Assert
    _mockAgeCategoryRepo.Verify(r => r.InsertUpdateEstabAgeCategoryDetail(It.IsAny<List<EstabAgeCategory>>()), Times.Never);
    Assert.False(result.IsSuccess);
    Assert.Equal(OTAErrorType.InvalidAgeCategory.ToInt(), result.ErrorCode);
}

场景4:请求数据为空集合

[Fact]
public void CreateResponse_EmptyRequest_ReturnsSuccess()
{
    // Arrange
    var inventoryRequest = new InventoryRequest { EstabAgeCategories = new List<EstabAgeCategory>() };
    var resultCount = 0;

    // Act
    var result = _sut.CreateResponse(inventoryRequest, resultCount);

    // Assert
    Assert.True(result.IsSuccess);
    Assert.Equal(0, result.ErrorCode);
}

可拓展的测试点

  • 测试EstabAgeCategories为null的场景(原代码直接调用Count会抛空引用异常,需验证或修改原代码做null处理)
  • 测试ProcessRequest返回其他错误码的场景(比如InernalError的其他枚举值)
  • 验证日志的错误级别输出(如果后续原代码添加错误日志)
  • 测试CreateResponse的所有分支逻辑(请求数>0但结果数等于请求数、请求数=0但结果数不为0等)
  • 验证Mock对象未被多余调用(比如仓储方法仅被调用一次,无重复调用)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 20:01:04