.NET Core 5.0下使用Moq模拟服务接口结合DbContext单元测试问题排查
问题排查与修复方案
核心问题梳理
- 测试方向完全错误:你要验证的是自己写的
InventoryService业务逻辑,反而直接Mock了IInventoryService接口,全程没有用到你实现的服务代码,测试跑的全是Moq的模拟返回,自然和你写的业务逻辑无关。 - 依赖Mock对象搞错:
InventoryService唯一的依赖是DBContext,你只需要MockDBContext以及对应的DbSet属性,再把Mock后的DBContext注入到真实的InventoryService实例中,才能真正测到业务代码。 - 缺少必要的模拟配置:
DeleteInventory方法中用到了DbSet.Find()方法,该方法不属于IQueryable接口,你目前只配置了IQueryable相关的四个属性,不单独Setup的话调用Find会默认返回null,导致删除逻辑直接返回false。 - 断言逻辑无效:比如创建库存的测试里断言
data不为空,data是你自己初始化的空列表,必然不为空,完全无法验证创建逻辑是否生效。
修正后的测试代码示例
public class InventoryTests { // 复用的Mock DbSet构建方法,避免每个测试重复写代码 private Mock<DbSet<T>> BuildMockDbSet<T>(IQueryable<T> data) where T : class { var mockSet = new Mock<DbSet<T>>(); mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); return mockSet; } [Fact] public void Get_user_inventories_with_valid_id() { // ARRANGE Guid theId1 = new("00000000-0000-0000-0000-000000000001"); string u1 = "xxx"; string u2 = "yyy"; var inventoryData = new List<Inventory> { new Inventory { Id = theId1, name = "Mums 1", userId = u1}, new Inventory { Id = Guid.NewGuid(), name = "Mums 1.2", userId = u1}, new Inventory { Id = Guid.NewGuid(), name = "Other user's inv", userId = u2} }.AsQueryable(); // Mock DbSet和DbContext var mockInventorySet = BuildMockDbSet(inventoryData); var mockDbContext = new Mock<DBContext>(); mockDbContext.Setup(c => c.Inventories).Returns(mockInventorySet.Object); // 初始化真实的被测服务,注入Mock的DbContext var inventoryService = new InventoryService(mockDbContext.Object); // ACT var tinvs = inventoryService.GetInventories(u1); // ASSERT Assert.Equal(2, tinvs.Count); Assert.All(tinvs, inv => Assert.Equal(u1, inv.userId)); } [Fact] public void Creating_An_Inventory() { // ARRANGE var inventoryData = Enumerable.Empty<Inventory>().AsQueryable(); var mockInventorySet = BuildMockDbSet(inventoryData); var mockDbContext = new Mock<DBContext>(); mockDbContext.Setup(c => c.Inventories).Returns(mockInventorySet.Object); var inventoryService = new InventoryService(mockDbContext.Object); var newInventory = new Inventory { Id = Guid.NewGuid(), name = "Mums 2", userId = "yyy" }; // ACT inventoryService.CreateInventory(newInventory); // ASSERT:验证Add和SaveChanges被正确调用,确认创建逻辑生效 mockInventorySet.Verify(s => s.Add(It.Is<Inventory>(i => i.Id == newInventory.Id)), Times.Once); mockDbContext.Verify(c => c.SaveChanges(), Times.Once); } [Fact] public void Deleting_An_Inventory() { // ARRANGE Guid targetId = new("00000000-0000-0000-0000-000000000001"); string u1 = "123"; var inventoryData = new List<Inventory> { new Inventory { Id = targetId, name = "testinv 1", userId = u1}, }.AsQueryable(); var mockInventorySet = BuildMockDbSet(inventoryData); // 单独Setup Find方法,匹配目标Id时返回对应数据 mockInventorySet.Setup(s => s.Find(targetId)).Returns(inventoryData.First(i => i.Id == targetId)); // 模拟Items DbSet,避免删除关联Item时报错 var mockItemSet = BuildMockDbSet(Enumerable.Empty<Item>().AsQueryable()); var mockDbContext = new Mock<DBContext>(); mockDbContext.Setup(c => c.Inventories).Returns(mockInventorySet.Object); mockDbContext.Setup(c => c.Items).Returns(mockItemSet.Object); var inventoryService = new InventoryService(mockDbContext.Object); // ACT var deleteResult = inventoryService.DeleteInventory(targetId, u1); // ASSERT Assert.True(deleteResult); mockInventorySet.Verify(s => s.Remove(It.Is<Inventory>(i => i.Id == targetId)), Times.Once); mockDbContext.Verify(c => c.SaveChanges(), Times.Once); } }
补充说明
测试增删逻辑时,不需要修改模拟的数据源然后判断数量,直接验证DbContext的对应方法是否被正确调用即可,符合单元测试隔离性的要求。如果需要测试关联数据删除逻辑,自行给Items的Mock DbSet配置对应测试数据即可。
内容的提问来源于stack exchange,提问作者Munashe M
相关产品推荐
相关产品推荐

