使用Xunit和Moq编写.NET Core中AgeCategoryRequestHandler单元测试的困惑
.NET Core中AgeCategoryRequestHandler的单元测试指导(Xunit+Moq)
现有测试代码的合理性与问题
合理之处
- 针对构造函数依赖(
IRepositoryResolver、ILogger)创建Mock对象,符合依赖注入的测试原则,避免耦合真实数据库或外部服务 - 使用AutoFixture自动生成测试数据,减少手动构造对象的重复工作
- 尝试验证仓储方法的调用次数,抓住了核心逻辑的测试点
存在的问题
- 错误Mock被测类:代码中
Mock<AgeCategoryRequestHandler>完全多余,我们要测试的是真实的类实例,而非Mock它 - 异步测试方法错误:
async void在Xunit中会导致测试不等待异步操作完成,必须改为async Task - 测试场景模糊:第一个测试方法的断言逻辑不对应明确的业务场景,无法验证核心业务规则
- 缺失关键Mock定义:第二个代码片段中
_mockRepository未定义,需要补充对应仓储接口(比如IEstabAgeCategoryRepository)的Mock - 未覆盖异常分支:没有测试仓储解析失败(返回null)的场景
- 未验证日志输出:原代码中有日志记录,测试中未验证日志是否正确触发
完整测试步骤与方法
步骤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
相关产品推荐
相关产品推荐

