GetDbSetByQuery().dbSet单元测试Mock异常求助
问题根源
你遇到的ArgumentNullException是因为被测代码调用的repository.GetDbSetByQuery().dbSet为null,而你在测试里完全没Mock这个方法,反而错误Mock了一个被测代码根本没用到的GetListAsync()方法,这是核心问题。
另外,测试里Mock的AutoMapper映射也是多余的——看被测方法的Select逻辑,它是手动构造NonStockedPartTrgZoneDto,根本没调用AutoMapper的Map方法,这部分Mock完全无效。
修复步骤
要解决这个问题,需要正确MockGetDbSetByQuery()方法,让它返回一个包含有效dbSet的对象,同时要让dbSet支持EF Core的Include和ToListAsync扩展方法。
1. 准备测试数据(带导航属性)
首先用Autofixture生成包含Part和Plant导航属性的实体集合,避免后续映射时出现空引用:
var fixture = new Fixture(); // 确保导航属性不为null,Autofixture默认会生成关联对象,这里显式指定更保险 var nonStockedPartTrgZoneList = fixture.Build<Domain.Definition.NonStockedPartTrgZone>() .With(x => x.Part, fixture.Create<Domain.Definition.Part>()) .With(x => x.Plant, fixture.Create<Domain.Definition.Plant>()) .CreateMany(3) .ToList(); // 转换为IQueryable,模拟EF的DbSet var queryable = nonStockedPartTrgZoneList.AsQueryable();
2. Mock支持EF扩展方法的DbSet
因为dbSet需要支持Include(EF扩展方法)和ToListAsync(异步查询),需要用NSubstitute创建一个同时实现IQueryable<T>和IAsyncEnumerable<T>的Mock对象:
// 先Mock GetDbSetByQuery返回的容器对象(假设类型是IDbSetQueryResult<T>,根据你的实际类型调整) var dbSetQueryResult = Substitute.For<IDbSetQueryResult<Domain.Definition.NonStockedPartTrgZone>>(); // 创建Mock DbSet,同时实现IQueryable和IAsyncEnumerable var mockDbSet = Substitute.For<IQueryable<Domain.Definition.NonStockedPartTrgZone>, IAsyncEnumerable<Domain.Definition.NonStockedPartTrgZone>>(); // 实现IQueryable的核心成员 ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).Provider.Returns(queryable.Provider); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).Expression.Returns(queryable.Expression); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).ElementType.Returns(queryable.ElementType); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).GetEnumerator().Returns(queryable.GetEnumerator()); // 实现IAsyncEnumerable的核心成员,支持ToListAsync ((IAsyncEnumerable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet) .GetAsyncEnumerator(default) .Returns(new AsyncEnumerator<Domain.Definition.NonStockedPartTrgZone>(queryable.GetEnumerator())); // 处理Include方法:因为Include是EF扩展方法,让Mock调用Include后返回自身即可 mockDbSet.Include(Arg.Any<Expression<Func<Domain.Definition.NonStockedPartTrgZone, object>>>()) .Returns(mockDbSet); // 让容器对象的dbSet属性指向我们的Mock DbSet dbSetQueryResult.dbSet.Returns(mockDbSet); // 让Repository的GetDbSetByQuery返回这个容器对象 _repository.GetDbSetByQuery().Returns(dbSetQueryResult);
注意:
AsyncEnumerator<T>是EF Core内部的异步枚举器,如果你引用了Microsoft.EntityFrameworkCore包,可以直接使用;如果没有,也可以自己实现一个简单的异步枚举器。
3. 构建预期的Dto列表
因为被测方法是手动构造Dto,所以测试里的预期列表要和被测逻辑保持一致,不要依赖AutoMapper:
var expectedDtoList = nonStockedPartTrgZoneList.Select(x => new NonStockedPartTrgZoneDto() { Id = x.Id, RfPart = x.RfPart, RfPlant = x.RfPlant, CdPart = x.Part.CdPart, CdPlant = x.Plant.CdPlant, SwActive = x.SwActive, NoZone = x.NoZone, InsertedDate = x.InsertedDate, InsertedUser = x.InsertedUser, UpdatedUser = x.UpdatedUser ?? x.InsertedUser, UpdatedDate = x.UpdatedDate ?? x.InsertedDate }).ToList();
4. 完整的修正后测试代码
[Fact] [Trait("Operation", "Get")] [Trait("Category", "NonStockedPartTrgZone")] public async Task NonStockedPartTrgZoneService_Get_ReturnsNonStockedPartTrgZoneDto() { // Arrange var fixture = new Fixture(); // 生成带导航属性的实体集合 var nonStockedPartTrgZoneList = fixture.Build<Domain.Definition.NonStockedPartTrgZone>() .With(x => x.Part, fixture.Create<Domain.Definition.Part>()) .With(x => x.Plant, fixture.Create<Domain.Definition.Plant>()) .CreateMany(3) .ToList(); var queryable = nonStockedPartTrgZoneList.AsQueryable(); // Mock GetDbSetByQuery返回的容器对象 var dbSetQueryResult = Substitute.For<IDbSetQueryResult<Domain.Definition.NonStockedPartTrgZone>>(); // Mock支持EF扩展的DbSet var mockDbSet = Substitute.For<IQueryable<Domain.Definition.NonStockedPartTrgZone>, IAsyncEnumerable<Domain.Definition.NonStockedPartTrgZone>>(); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).Provider.Returns(queryable.Provider); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).Expression.Returns(queryable.Expression); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).ElementType.Returns(queryable.ElementType); ((IQueryable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet).GetEnumerator().Returns(queryable.GetEnumerator()); ((IAsyncEnumerable<Domain.Definition.NonStockedPartTrgZone>)mockDbSet) .GetAsyncEnumerator(default) .Returns(new AsyncEnumerator<Domain.Definition.NonStockedPartTrgZone>(queryable.GetEnumerator())); // 处理Include方法 mockDbSet.Include(Arg.Any<Expression<Func<Domain.Definition.NonStockedPartTrgZone, object>>>()) .Returns(mockDbSet); dbSetQueryResult.dbSet.Returns(mockDbSet); _repository.GetDbSetByQuery().Returns(dbSetQueryResult); // 构建预期Dto列表,和被测逻辑一致 var expectedDtoList = nonStockedPartTrgZoneList.Select(x => new NonStockedPartTrgZoneDto() { Id = x.Id, RfPart = x.RfPart, RfPlant = x.RfPlant, CdPart = x.Part.CdPart, CdPlant = x.Plant.CdPlant, SwActive = x.SwActive, NoZone = x.NoZone, InsertedDate = x.InsertedDate, InsertedUser = x.InsertedUser, UpdatedUser = x.UpdatedUser ?? x.InsertedUser, UpdatedDate = x.UpdatedDate ?? x.InsertedDate }).ToList(); // Act var result = await _nonStockedPartTrgZoneService.Get(); // Assert result.Should().NotBeNull(); result.IsSuccessful.Should().BeTrue(); result.Result.Should().BeEquivalentTo(expectedDtoList); }
关键注意点
- 一定要Mock被测代码实际调用的方法,不要凭感觉Mock其他方法。
- 当Mock EF的DbSet时,必须同时实现
IQueryable<T>和IAsyncEnumerable<T>才能支持异步查询方法。 - 手动构造Dto的场景下,不要依赖AutoMapper的Mock,直接和被测逻辑保持一致构建预期结果更可靠。
内容的提问来源于stack exchange,提问作者sedya

