如何在.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
相关产品推荐
相关产品推荐

