EF Core内存单元测试失败:Create-and-Attach删除方法测试异常
EF Core创建并附加式删除的单元测试问题解决建议
问题背景
为提升性能,仓储的DeleteAsync方法采用**创建并附加(create-and-attach)**的EF Core删除方式,而非先读取再删除。该方法在生产环境使用SQL Server时正常,但单元测试中使用InMemory数据库或Sqlite内存数据库时删除操作未生效,测试断言失败。
仓储核心代码:
public async Task DeleteAsync(int id) { // DELETE: EF CORE create-and-attach approach Vehicle vehicleToDelete = new Vehicle() { Id = id }; _context.Entry(vehicleToDelete).State = EntityState.Deleted; await _context.SaveChangesAsync(); }
问题原因
InMemory数据库特性限制
InMemory数据库是为测试设计的轻量级实现,当通过附加实体标记Deleted并调用SaveChangesAsync时,它只会记录删除操作,但不会自动从当前上下文的DbSet中移除该实体。直接访问repo.Vehicles.Count()会包含仍被上下文跟踪的已标记删除实体,导致断言错误。
Sqlite内存数据库使用误区
Sqlite内存数据库的生命周期与数据库连接绑定,连接关闭时数据会被销毁。测试中未显式打开连接,且未确保架构创建,导致数据无法持久化或查询失败。
解决方案
方案1:适配InMemory数据库的测试修改
修改测试中的断言逻辑,通过AsNoTracking()绕过上下文跟踪,直接查询数据库中的实际数据:
[Fact] public async Task Can_Delete_Vehicle_With_InMemory() { // Arrange var options = new DbContextOptionsBuilder<FredsCarsDbContext>() .UseInMemoryDatabase($"FredCars-{Guid.NewGuid().ToString()}") .Options; using var context = new FredsCarsDbContext(options); var repo = new EFVehicleRepository(context); Vehicle vehicle1 = new Vehicle { Status = Status.New, Year = "2025", Make = "Make", Model = "Gladiator Rubicon 4 X 4", Color = "Joose", Price = 64125, VIN = "1C6RJTBG0SL532163", VehicleTypeId = 3, ImagePath = "/images/jeeps/jeep5.jpg" }; Vehicle vehicle2 = new Vehicle { Status = Status.Used, Year = "2020", Make = "Ford", Model = "Escape", Color = "Joose", Price = 22999, VIN = "1FMCU0F63LUC25826", VehicleTypeId = 1, ImagePath = "/images/cars/car2.webp" }; await repo.CreateAsync(vehicle1); await repo.CreateAsync(vehicle2); // 用AsNoTracking验证初始数量 Assert.Equal(2, await repo.Vehicles.AsNoTracking().CountAsync()); // Act await repo.DeleteAsync(vehicle2.Id); // Assert // 绕过上下文跟踪,查询数据库实际数据 Assert.Equal(1, await repo.Vehicles.AsNoTracking().CountAsync()); var remainingVehicle = await repo.Vehicles.AsNoTracking().FirstOrDefaultAsync(v => v.Id == vehicle1.Id); Assert.Equal(vehicle1.VIN, remainingVehicle?.VIN); }
方案2:正确使用Sqlite内存数据库
显式打开数据库连接,并确保创建数据库架构,避免连接关闭导致数据丢失:
[Fact] public async Task Can_Delete_Vehicle_With_Sqlite() { // Arrange var options = new DbContextOptionsBuilder<FredsCarsDbContext>() .UseSqlite("DataSource=:memory:") .Options; using var context = new FredsCarsDbContext(options); // 必须显式打开连接,否则内存数据库会被销毁 await context.Database.OpenConnectionAsync(); // 创建数据库架构 await context.Database.EnsureCreatedAsync(); var repo = new EFVehicleRepository(context); Vehicle vehicle1 = new Vehicle { Status = Status.New, Year = "2025", Make = "Make", Model = "Gladiator Rubicon 4 X 4", Color = "Joose", Price = 64125, VIN = "1C6RJTBG0SL532163", VehicleTypeId = 3, ImagePath = "/images/jeeps/jeep5.jpg" }; Vehicle vehicle2 = new Vehicle { Status = Status.Used, Year = "2020", Make = "Ford", Model = "Escape", Color = "Joose", Price = 22999, VIN = "1FMCU0F63LUC25826", VehicleTypeId = 1, ImagePath = "/images/cars/car2.webp" }; await repo.CreateAsync(vehicle1); await repo.CreateAsync(vehicle2); Assert.Equal(2, await repo.Vehicles.CountAsync()); // Act await repo.DeleteAsync(vehicle2.Id); // Assert Assert.Equal(1, await repo.Vehicles.CountAsync()); var remainingVehicle = await repo.Vehicles.FirstOrDefaultAsync(v => v.Id == vehicle1.Id); Assert.Equal(vehicle1.VIN, remainingVehicle?.VIN); }
额外注意点
- 若要更贴近生产环境行为,优先选择Sqlite内存数据库,它比InMemory更接近真实数据库的SQL执行逻辑。
- 测试中若涉及多上下文操作,Sqlite内存库需要确保所有操作共享同一个打开的连接。
内容的提问来源于stack exchange,提问作者Sam
相关产品推荐
相关产品推荐

