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

EF Core xunit单元测试如何不连接数据库断言Include方法被调用

测试IQueryable是否调用过Include()的无数据库方案

方案1:解析表达式树验证(推荐)

EF Core的Include()/ThenInclude()调用会在IQueryable<T>的Expression属性中生成对应的方法调用节点,无需执行查询,直接遍历表达式树即可验证是否存在指定的Include操作,完全不需要依赖任何数据库(包括InMemory),也不需要构造DbContext。

首先实现通用的验证辅助方法:

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;

public static class QueryableIncludeValidator
{
    /// <summary>
    /// 检查IQueryable是否已调用指定路径的Include
    /// </summary>
    /// <param name="navigationPropertyPath">导航属性路径,支持嵌套,比如"User.Profile"</param>
    public static bool HasInclude<T>(this IQueryable<T> query, string navigationPropertyPath)
    {
        return VisitExpression(query.Expression, navigationPropertyPath);
    }

    private static bool VisitExpression(Expression expression, string targetPath)
    {
        if (expression is MethodCallExpression methodCall)
        {
            // 匹配EF Core的Include/ThenInclude方法
            if (methodCall.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
                && (methodCall.Method.Name == nameof(EntityFrameworkQueryableExtensions.Include) 
                    || methodCall.Method.Name == nameof(EntityFrameworkQueryableExtensions.ThenInclude)))
            {
                var pathArg = methodCall.Arguments[1];
                // 处理字符串参数形式的Include:Include("User")
                if (pathArg is ConstantExpression constantArg && constantArg.Value?.ToString() == targetPath)
                {
                    return true;
                }
                // 处理强类型参数形式的Include:Include(u => u.User)
                if (pathArg is UnaryExpression unaryArg && unaryArg.Operand is LambdaExpression lambda)
                {
                    var actualPath = GetPropertyPath(lambda);
                    if (actualPath == targetPath)
                    {
                        return true;
                    }
                }
            }
            // 递归检查内层表达式
            return VisitExpression(methodCall.Arguments[0], targetPath);
        }
        return false;
    }

    // 从Lambda表达式提取属性路径
    private static string GetPropertyPath(LambdaExpression lambda)
    {
        var memberStack = new Stack<string>();
        var current = lambda.Body;
        while (current is MemberExpression member)
        {
            memberStack.Push(member.Member.Name);
            current = member.Expression;
        }
        return string.Join(".", memberStack);
    }
}

单元测试使用示例:

[Test]
public void VerifyIncludeCall()
{
    // 构造空的IQueryable作为初始输入,不需要关联任何DbContext
    var initialQuery = Enumerable.Empty<Order>().AsQueryable();
    // 调用你自己实现的扩展方法
    var resultQuery = initialQuery.YourCustomExtensionMethod();

    // 验证是否Include了指定导航属性
    Assert.True(resultQuery.HasInclude(nameof(Order.User)));
    // 验证嵌套Include
    Assert.True(resultQuery.HasInclude("User.Profile"));
}

方案2:Mock IQueryProvider捕获调用

如果你需要验证Include的调用次数、调用顺序等更细节的逻辑,可以用Mock框架模拟IQueryProvider来捕获所有查询操作:

using Moq;
using System.Linq.Expressions;

[Test]
public void VerifyIncludeCallCount()
{
    var mockProvider = new Mock<IQueryProvider>();
    var initialQuery = new EnumerableQuery<Order>(Array.Empty<Order>());

    mockProvider.Setup(p => p.CreateQuery<Order>(It.IsAny<Expression>()))
        .Returns<Expression>(expr => new EnumerableQuery<Order>(expr));

    var testQuery = (IQueryable<Order>)mockProvider.Object.CreateQuery<Order>(initialQuery.Expression);
    // 调用自定义扩展方法
    var result = testQuery.YourCustomExtensionMethod();

    // 验证Include("User")恰好被调用1次
    mockProvider.Verify(p => p.CreateQuery<Order>(It.Is<Expression>(expr => 
        expr.ToString().Contains("Include(\"User\")"))), Times.Once);
}

注意事项

  • 两种方案都完全不需要引入任何数据库相关依赖,测试执行速度极快
  • 支持验证强类型、字符串参数两种形式的Include,也支持嵌套的ThenInclude校验
  • 无需构造DbContext或DbSet实例,直接用Enumerable.Empty<T>().AsQueryable()作为初始查询对象即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 23:18:02