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

如何在.NET 6中为EF Core的Include/ThenInclude编写Moq单元测试桩?

解决方案:Mock EF Core 的 Include/ThenInclude 方法

方法1:预加载测试数据的导航属性

如果测试数据是固定的,直接在构建测试对象时手动关联导航属性,这样无论代码中是否调用 Include,返回的对象都已包含关联数据。

// 构建关联的测试数据
var testNotifications = new List<Notification>
{
    new Notification { Id = 1, UserId = 1, Content = "测试通知" }
};
var testUsers = new List<User>
{
    new User { Id = 1, Name = "测试用户", Notifications = testNotifications }
};

// 创建Mock DbSet并注入到DbContext
var userDbSetMock = new DbSetMock<User>(testUsers);
var dbContextMock = new DbContextMock<YourDbContext>();
dbContextMock.Setup(db => db.Users).Returns(userDbSetMock.Object);

适合简单场景,无需处理复杂的链式 Include/ThenInclude。

方法2:自定义 QueryProvider 拦截 Include/ThenInclude

通过实现自定义 IQueryProvider,拦截 Include 和 ThenInclude 的表达式,手动加载对应导航属性。这种方式支持链式调用。

第一步:实现自定义 QueryProvider 和 Queryable

public class IncludeAwareQueryProvider<T> : IQueryProvider
{
    private readonly IQueryProvider _innerProvider;
    private readonly Action<Expression> _processIncludeLogic;

    public IncludeAwareQueryProvider(IQueryProvider innerProvider, Action<Expression> processIncludeLogic)
    {
        _innerProvider = innerProvider;
        _processIncludeLogic = processIncludeLogic;
    }

    public IQueryable CreateQuery(Expression expression)
    {
        _processIncludeLogic?.Invoke(expression);
        return new IncludeAwareQueryable<T>(_innerProvider.CreateQuery(expression), this);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        _processIncludeLogic?.Invoke(expression);
        return new IncludeAwareQueryable<TElement>(_innerProvider.CreateQuery<TElement>(expression), this);
    }

    public object Execute(Expression expression) => _innerProvider.Execute(expression);
    public TResult Execute<TResult>(Expression expression) => _innerProvider.Execute<TResult>(expression);
}

public class IncludeAwareQueryable<T> : IQueryable<T>
{
    private readonly IQueryable<T> _innerQuery;
    private readonly IncludeAwareQueryProvider<T> _provider;

    public IncludeAwareQueryable(IQueryable<T> innerQuery, IncludeAwareQueryProvider<T> provider)
    {
        _innerQuery = innerQuery;
        _provider = provider;
    }

    public IEnumerator<T> GetEnumerator() => _innerQuery.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    public Type ElementType => _innerQuery.ElementType;
    public Expression Expression => _innerQuery.Expression;
    public IQueryProvider Provider => _provider;
}

第二步:在测试中使用自定义 Provider

var testUsers = new List<User> { new User { Id = 1, Name = "测试用户" } }.AsQueryable();
var testNotifications = new List<Notification> { new Notification { Id = 1, UserId = 1, Content = "测试通知" } };

var userDbSetMock = new Mock<DbSet<User>>();
userDbSetMock.As<IQueryable<User>>().Setup(m => m.Provider)
    .Returns(new IncludeAwareQueryProvider<User>(testUsers.Provider, expr =>
    {
        // 解析Include/ThenInclude表达式,加载对应导航属性
        var methodCallExpr = expr as MethodCallExpression;
        if (methodCallExpr?.Method.Name is "Include" or "ThenInclude")
        {
            // 提取导航属性名称(可根据实际表达式结构调整解析逻辑)
            var propertyPath = (methodCallExpr.Arguments[1] as LambdaExpression)?
                .Body.ToString()
                .Split('.')
                .Last();

            if (propertyPath == nameof(User.Notifications))
            {
                foreach (var user in testUsers)
                {
                    user.Notifications = testNotifications.Where(n => n.UserId == user.Id).ToList();
                }
            }
            // 如需支持更多导航属性,继续添加判断逻辑即可
        }
    }));

// 完成DbSet的其他Mock设置
userDbSetMock.As<IQueryable<User>>().Setup(m => m.Expression).Returns(testUsers.Expression);
userDbSetMock.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(testUsers.ElementType);
userDbSetMock.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(testUsers.GetEnumerator());

// 注入到DbContext
var dbContextMock = new Mock<YourDbContext>();
dbContextMock.Setup(db => db.Users).Returns(userDbSetMock.Object);

方法3:利用 EntityFrameworkCoreMock 的导航属性配置

EntityFrameworkCoreMock 本身支持配置导航属性关联,可通过 WithNavigation 方法自动加载关联数据:

var dbContextMock = new DbContextMock<YourDbContext>();

// 分别创建两个DbSet的Mock
var usersDbSetMock = dbContextMock.CreateDbSetMock(db => db.Users, new List<User>
{
    new User { Id = 1, Name = "测试用户" }
});

var notificationsDbSetMock = dbContextMock.CreateDbSetMock(db => db.Notifications, new List<Notification>
{
    new Notification { Id = 1, UserId = 1, Content = "测试通知" }
});

// 配置导航属性的关联规则
usersDbSetMock.WithNavigation(u => u.Notifications, u => notificationsDbSetMock.Object.Where(n => n.UserId == u.Id));

这样调用 Include(u => u.Notifications) 时,Mock会自动返回关联的通知数据。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 22:55:18