使用Moq对Mock<DbSet<T>>的ToListAsync方法进行模拟
这个坑我踩过!你遇到的ThrowIfSetupExpressionInvolvesUnsupportedMember异常,核心原因是**ToListAsync是EF(Core/6)的扩展方法**,Moq根本没办法直接对扩展方法做Setup——Moq只能拦截接口或类的虚方法、抽象方法,而扩展方法本质是静态方法,不属于DbSet<T>本身的成员,自然没法被Moq拦截。
正确的解决方案:让模拟DbSet支持异步查询接口
EF的异步扩展方法(比如ToListAsync)是基于IDbAsyncEnumerable(EF6)或IAsyncEnumerable(EF Core)接口工作的。我们只需要让Mock的DbSet<T>实现这些接口,就能让ToListAsync正常工作,不需要单独去Setup这个扩展方法。
下面是通用的辅助方法,帮你把List转换成支持异步查询的Mock DbSet:
using Microsoft.EntityFrameworkCore.Query.Internal; using System.Collections.Generic; using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading; using System.Threading.Tasks; using Moq; public static class MockDbSetHelper { public static Mock<DbSet<T>> CreateMockDbSet<T>(List<T> data) where T : class { var queryableData = data.AsQueryable(); var mockDbSet = new Mock<DbSet<T>>(); // 配置同步查询的IQueryable接口 mockDbSet.As<IQueryable<T>>() .Setup(m => m.Provider) .Returns(queryableData.Provider); mockDbSet.As<IQueryable<T>>() .Setup(m => m.Expression) .Returns(queryableData.Expression); mockDbSet.As<IQueryable<T>>() .Setup(m => m.ElementType) .Returns(queryableData.ElementType); mockDbSet.As<IQueryable<T>>() .Setup(m => m.GetEnumerator()) .Returns(queryableData.GetEnumerator()); // 配置EF Core需要的IAsyncEnumerable接口 mockDbSet.As<IAsyncEnumerable<T>>() .Setup(m => m.GetAsyncEnumerator(CancellationToken.None)) .Returns(new TestAsyncEnumerator<T>(queryableData.GetEnumerator())); // 如果是EF6,还需要配置IDbAsyncEnumerable接口 mockDbSet.As<IDbAsyncEnumerable<T>>() .Setup(m => m.GetAsyncEnumerator()) .Returns(new TestDbAsyncEnumerator<T>(queryableData.GetEnumerator())); return mockDbSet; } // 辅助类:实现EF Core的IAsyncEnumerator private class TestAsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _innerEnumerator; public TestAsyncEnumerator(IEnumerator<T> innerEnumerator) { _innerEnumerator = innerEnumerator; } public T Current => _innerEnumerator.Current; public ValueTask<bool> MoveNextAsync() { return new ValueTask<bool>(_innerEnumerator.MoveNext()); } public ValueTask DisposeAsync() { _innerEnumerator.Dispose(); return ValueTask.CompletedTask; } } // 辅助类:实现EF6的IDbAsyncEnumerator private class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T> { private readonly IEnumerator<T> _innerEnumerator; public TestDbAsyncEnumerator(IEnumerator<T> innerEnumerator) { _innerEnumerator = innerEnumerator; } public T Current => _innerEnumerator.Current; object IDbAsyncEnumerator.Current => Current; public Task<bool> MoveNextAsync(CancellationToken cancellationToken) { return Task.FromResult(_innerEnumerator.MoveNext()); } public void Dispose() { _innerEnumerator.Dispose(); } } }
怎么使用这个辅助方法?
把你原来的代码改成这样就行:
// 用辅助方法创建支持异步的Mock DbSet var mockSomeTable = MockDbSetHelper.CreateMockDbSet(ListOfItems); // 配置你的DbContext MockedDatabaseContext.Setup(u => u.SomeTable).Returns(mockSomeTable.Object);
这样你的目标方法里调用await _dbContext.SomeTable.ToListAsync()时,就会正常返回你传入的ListOfItems,不需要再单独去SetupToListAsync了。
为什么原来的写法不行?
再啰嗦两句:ToListAsync是QueryableExtensions类里的静态方法,它的实现是调用IQueryable.Provider的异步方法来获取结果。你直接去Setupd => d.ToListAsync(...),Moq会识别出这是个扩展方法,不是DbSet<T>的成员,所以直接抛出异常告诉你不支持这种Setup。
简化方案(可选)
如果你不想自己写这些辅助代码,可以用第三方库,比如EntityFrameworkCore.Testing.Moq(EF Core)或Moq.EntityFramework(EF6),这些库已经封装好了模拟DbSet的逻辑,一行代码就能搞定:
// 用EntityFrameworkCore.Testing.Moq的例子 var mockDbContext = new Mock<YourDbContext>(); mockDbContext.SetupDbSet(c => c.SomeTable, ListOfItems);
内容的提问来源于stack exchange,提问作者Jeroen

