使用MockQueryable.Moq测试AddRangeAsync时结果始终返回0的问题
解决MockQueryable.Moq测试AddRangeAsync返回0的问题
问题根源
- Mock DbSet未绑定动态数据源:初始通过
initProducts.AsQueryable().BuildMockDbSet()创建的Mock DbSet,其底层Queryable绑定的是创建时的空列表快照,后续initProducts添加数据后,Mock DbSet的查询接口(如Count())仍会返回初始空列表的结果。 - 未Mock
SaveChangesAsync:服务中调用了_myDb.SaveChangesAsync(),但测试未对该方法做模拟,EF上下文的状态变更逻辑未被正确触发。
解决步骤
步骤1:让Mock DbSet动态返回最新数据源
手动配置Mock DbSet的IQueryable接口,使其每次被查询时都返回当前initProducts的最新状态,替代MockQueryable的静态快照创建方式:
var initProducts = new List<Product>(); var mockProducts = new Mock<DbSet<Product>>(); // 动态绑定IQueryable到最新的initProducts mockProducts.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(() => initProducts.AsQueryable().Provider); mockProducts.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(() => initProducts.AsQueryable().Expression); mockProducts.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(() => initProducts.AsQueryable().ElementType); mockProducts.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(() => initProducts.AsQueryable().GetEnumerator());
步骤2:完善方法Mock逻辑
- 保留
AddRangeAsync的回调逻辑,确保新实体被加入initProducts - 模拟
SaveChangesAsync的成功返回,匹配服务中的调用逻辑:
// Mock AddRangeAsync,添加回调并返回完成任务 mockProducts .Setup(x => x.AddRangeAsync(It.IsAny<IEnumerable<Product>>(), It.IsAny<CancellationToken>())) .Callback((IEnumerable<Product> products, CancellationToken _) => initProducts.AddRange(products)) .Returns(Task.CompletedTask); var mockDb = new Mock<MyDb>(); mockDb.Setup(x => x.Products).Returns(mockProducts.Object); // Mock SaveChangesAsync,返回受影响行数2 mockDb.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(2);
完整修改后的测试代码
[Fact] public async Task AddRangeTest() { var initProducts = new List<Product>(); var mockProducts = new Mock<DbSet<Product>>(); // 配置DbSet动态返回最新数据 mockProducts.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(() => initProducts.AsQueryable().Provider); mockProducts.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(() => initProducts.AsQueryable().Expression); mockProducts.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(() => initProducts.AsQueryable().ElementType); mockProducts.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(() => initProducts.AsQueryable().GetEnumerator()); // Mock AddRangeAsync mockProducts .Setup(x => x.AddRangeAsync(It.IsAny<IEnumerable<Product>>(), It.IsAny<CancellationToken>())) .Callback((IEnumerable<Product> products, CancellationToken _) => initProducts.AddRange(products)) .Returns(Task.CompletedTask); var mockDb = new Mock<MyDb>(); mockDb.Setup(x => x.Products).Returns(mockProducts.Object); // Mock SaveChangesAsync mockDb.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(2); var productService = new ProductService(mockDb.Object); await productService.AddRange(); Assert.Equal(2, mockDb.Object.Products.Count()); }
补充说明
如果希望继续使用MockQueryable.Moq,需要在initProducts更新后重新创建Mock DbSet并重新绑定到MyDb,但这种方式不如手动MockIQueryable灵活,因此更推荐上述手动配置的方案。
内容的提问来源于stack exchange,提问作者Vincent
相关产品推荐
相关产品推荐

