如何在Ninject中通过SetUp填充模拟数据库,测试Specification查询方法
看起来你已经搭好了基础框架,现在卡壳在让基于IQueryable的查询方法能在内存测试数据上跑起来——其实核心就是把你的FakeDbSet完善成真正支持LINQ查询的实现,再把它注入到Mock DbContext里就行,下面一步步说具体怎么做:
1. 先把FakeDbSet改成支持IQueryable的版本
你之前的FakeDbSet可能只是继承了DbSet,但没实现IQueryable的核心接口,导致Where、OrderBy这些LINQ方法没法在内存集合上执行。给它加个内部List来存测试数据,再把IQueryable的接口委托给这个List的AsQueryable()结果:
public class FakeDbSet<T> : DbSet<T>, IQueryable where T : class { private readonly List<T> _inMemoryData; private readonly IQueryable _queryableData; public FakeDbSet() { _inMemoryData = new List<T>(); _queryableData = _inMemoryData.AsQueryable(); } // 重写DbSet的基础操作方法,直接操作内存列表 public override T Add(T entity) { _inMemoryData.Add(entity); return entity; } public override T Remove(T entity) { _inMemoryData.Remove(entity); return entity; } // 实现IQueryable的核心属性,让LINQ查询能正常工作 public override Type ElementType => _queryableData.ElementType; public override Expression Expression => _queryableData.Expression; public override IQueryProvider Provider => _queryableData.Provider; // 重写枚举器,返回内存列表的枚举器 public override IEnumerator<T> GetEnumerator() { return _inMemoryData.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
这个版本的FakeDbSet完全兼容IQueryable,所有LINQ查询都会直接在内存列表上执行,和真实DbContext的行为一致。
2. 在SetUp中配置Mock DbContext和注入测试数据
接下来在测试类的SetUp方法里,把填充好测试数据的FakeDbSet绑定到你的DbContext的对应属性上,再通过Ninject把DbContext注入到服务中:
private ICreditorService _creditorService; [SetUp] public void TestSetup() { // 1. 创建FakeDbSet并填充预设的Creditor数据 var fakeCreditorSet = new FakeDbSet<Creditor>(); fakeCreditorSet.Add(new Creditor { Id = 1, Name = "Alpha Corp", IsActive = true }); fakeCreditorSet.Add(new Creditor { Id = 2, Name = "Beta LLC", IsActive = false }); fakeCreditorSet.Add(new Creditor { Id = 3, Name = "Gamma Inc", IsActive = true }); // 2. Mock你的DbContext,让Creditors属性返回上面的FakeDbSet var mockDbContext = new Mock<YourDbContext>(); mockDbContext.Setup(ctx => ctx.Creditors).Returns(fakeCreditorSet); // 如果你的BuildQuery用到了Include,还要模拟Include方法 mockDbContext.Setup(ctx => ctx.Creditors.Include(It.IsAny<string>())) .Returns(fakeCreditorSet); // 3. 配置Ninject,把DbContext绑定到这个Mock实例 var kernel = new StandardKernel(); kernel.Bind<YourDbContext>().ToConstant(mockDbContext.Object); kernel.Bind<ICreditorService>().To<CreditorService>(); // 4. 从Ninject获取服务实例,用于后续测试 _creditorService = kernel.Get<ICreditorService>(); }
现在当你的服务调用_db.Creditors时,拿到的就是你预设的内存集合,BuildQuery构建的IQueryable查询会直接在这个集合上跑,完全不需要真实数据库。
3. 编写测试用例验证GetBySpecification和BuildQuery
现在你可以直接测试这些依赖IQueryable的方法了,比如验证某个Specification过滤后的结果是否符合预期:
[Test] public void GetBySpecification_ActiveCreditors_ReturnsOnlyActiveEntries() { // Arrange:SetUp已经帮你填充好测试数据了 var activeSpec = new ActiveCreditorsSpecification(); // Act var result = _creditorService.GetBySpecification(activeSpec).ToList(); // Assert Assert.That(result.Count, Is.EqualTo(2)); Assert.That(result.All(c => c.IsActive), Is.True); }
这个测试不仅验证了GetBySpecification的逻辑,也间接验证了BuildQuery是否正确把Specification转换成了对应的LINQ查询。
4. 处理进阶场景(可选)
如果你的服务用到了SaveChanges,记得在Mock DbContext里模拟这个方法:
mockDbContext.Setup(ctx => ctx.SaveChanges()).Returns(1); // 返回受影响的行数
如果用到了更复杂的导航属性查询,你可以在FakeDbSet里提前填充关联数据,或者用Moq进一步模拟Include的强类型重载。
内容的提问来源于stack exchange,提问作者Lars Holdgaard

