xUnit测试中Mock对象在控制器测试失效,返回404问题排查
问题背景
项目基于通用仓储(Generic Repository)和工作单元(Unit of Work)模式开发API,包含Category表。单独Mock UnitOfWork时功能正常,但测试CategoriesController的GetCategoryById接口时始终返回404状态码,推测是Mock配置错误但无法定位。
核心原因
Moq默认使用引用相等匹配方法参数:测试中Setup时传入的是特定的GetCategoriesWithParentsSpecification(1)实例,但控制器内部会创建一个全新的GetCategoriesWithParentsSpecification(id)实例,两者是不同对象,导致Mock不匹配,返回默认值(null)。后续控制器中直接访问category.Id触发空引用异常,进入catch块返回错误,最终表现为404(实际是异常处理逻辑导致的返回结果偏差)。
解决方案
1. 修正Mock参数匹配逻辑
将测试中的Setup改为匹配任意GetCategoriesWithParentsSpecification实例,或匹配符合业务条件的实例:
方式一:匹配任意该类型实例(简单直接)
_unitOfWork.Setup(x => x.Repository<Category>().GetEntityWithSpec(It.IsAny<GetCategoriesWithParentsSpecification>())) .ReturnsAsync(newCategory) .Verifiable();
方式二:匹配指定Id的实例(更精确)
如果GetCategoriesWithParentsSpecification类公开了用于筛选的Id属性,可以用条件匹配:
_unitOfWork.Setup(x => x.Repository<Category>().GetEntityWithSpec(It.Is<GetCategoriesWithParentsSpecification>(spec => spec.TargetId == 1))) .ReturnsAsync(newCategory) .Verifiable();
注:将spec.TargetId替换为实际类中存储筛选Id的属性名
2. 修复控制器空引用风险
在控制器中先判断category是否为null,再检查Id,避免空引用异常:
var category = await _unitOfWork.Repository<Category>().GetEntityWithSpec(spec); if (category == null || category.Id == 0) return NotFound(new ApiResponse(404));
3. 补充Mapper的Mock配置
测试中需MockIMapper的Map方法,否则返回的CategoryToReturnDto会为null,影响断言:
var categoryDto = new CategoryToReturnDto { Id = newCategory.Id, Name = newCategory.Name }; _mapper.Setup(m => m.Map<Category, CategoryToReturnDto>(newCategory)).Returns(categoryDto);
修正后的完整测试代码
public class CategoriesControllerTests { private readonly Mock<IMapper> _mapper; private readonly Mock<IUnitOfWork> _unitOfWork; public CategoriesControllerTests() { _mapper = new Mock<IMapper>(); _unitOfWork = new Mock<IUnitOfWork>(); } [Fact] public async Task Get_OK_ObjectResult_CategoryById() { //Arrange Category newCategory = CreateTestCategory(); var categoryDto = new CategoryToReturnDto { Id = newCategory.Id, Name = newCategory.Name }; // 修正Mock参数匹配 _unitOfWork.Setup(x => x.Repository<Category>().GetEntityWithSpec(It.IsAny<GetCategoriesWithParentsSpecification>())) .ReturnsAsync(newCategory) .Verifiable(); // Mock Mapper转换 _mapper.Setup(m => m.Map<Category, CategoryToReturnDto>(newCategory)).Returns(categoryDto); var controller = new CategoriesController(_unitOfWork.Object, _mapper.Object); //Act var result = await controller.GetCategoryById(1); // Assert var okResult = result.Result as OkObjectResult; okResult.ShouldNotBeNull(); var apiResponse = okResult.Value as ApiResponse; apiResponse.ShouldNotBeNull(); apiResponse.StatusCode.ShouldBe(200); var returnedDto = apiResponse.Data as CategoryToReturnDto; returnedDto.ShouldNotBeNull(); returnedDto.Id.ShouldBe(newCategory.Id); returnedDto.Name.ShouldBe(newCategory.Name); } private Category CreateTestCategory() { return new Category() { Id = 1, Priority = 1, Name = "Test Category", Enabled = true, Details = "Testing category data" }; } }
内容的提问来源于stack exchange,提问作者Arash Yazdani

