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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 20:08:10