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
相关产品推荐
相关产品推荐

