EF Core InMemory数据库测试 串行执行抛出DbUpdateConcurrencyException
问题根因
你的猜测方向完全正确,核心问题是静态测试实体实例复用+DbContext生命周期设计缺陷共同导致的:
- 静态TestData类持有的是全局共享的实体实例,即使上一个测试执行完调用了
ChangeTracker.Clear()、分离了当前上下文的所有追踪条目、甚至删除了内存数据库,这些静态实体实例本身的属性值、导航属性引用、EF Core之前写入的影子属性(比如主键值、外键关联标记)都已经被修改,不会被重置。下一个测试把这些被其他上下文追踪过的实例Add到新上下文时,EF Core扫描到实例带有非默认的主键值,会错误将关联的Tag等实体标记为Modified状态,但全新的内存数据库里根本没有对应主键的记录,执行SaveChanges时就会抛出你看到的DbUpdateConcurrencyException。 - 现有测试基类在构造函数中初始化DbContext,且所有测试共用固定名称的InMemory数据库,一旦清理逻辑出现遗漏,很容易出现跨测试用例的数据污染。你之前尝试在TearDown中遍历Detach所有实体的方案无效,是因为该操作只能清理当前上下文的追踪关系,无法修复静态实例本身被修改的内部状态。
快速排查验证方法
- 把测试中直接引用
TestData.AlexItemA、TestData.TagB的地方,改成临时new的全新实体,属性值和TestData里的固定值保持一致,跑全量串行测试,如果异常消失就可以确认根因。 - 在报错的
_unitOfWork.CompleteAsync()调用前加断点,查看_context.ChangeTracker.Entries()中所有实体的State,会发现传入的Tag实体状态为Modified,但当前数据库中不存在对应Id的Tag记录,和异常堆栈的报错逻辑完全匹配。 - 检查Repository和UnitOfWork的初始化逻辑,确认它们没有持有跨测试用例复用的DbContext实例。
EF Core InMemory 测试最优实现方案
按照下面的逻辑调整测试基类和测试数据写法,可以彻底避免跨测试污染问题:
测试基类改造
using System; using System.Threading.Tasks; using Application; using Microsoft.EntityFrameworkCore; using NUnit.Framework; namespace UnitTests.Repositories; public class RepositoryTestBase { protected ApplicationDbContext _context; protected IItemRepository _ItemRepository; protected IUnitOfWork _unitOfWork; // 每个测试用例执行前独立初始化所有依赖 [SetUp] public void Setup() { var dbOptions = new DbContextOptionsBuilder<ApplicationDbContext>() // 每个测试用例使用全局唯一的数据库名,从根源避免跨用例数据冲突 .UseInMemoryDatabase(databaseName: $"Test_Repo_{Guid.NewGuid():N}") .Options; _context = new ApplicationDbContext(dbOptions); // 每次测试重新初始化仓储和工作单元,确保持有当前测试的上下文实例 _ItemRepository = new ItemRepository(_context); _unitOfWork = new UnitOfWork(_context); } [TearDown] public async Task TearDown() { // 测试结束直接释放上下文即可,不需要额外执行删库建库操作 await _context.DisposeAsync(); } }
测试数据规范
- 静态TestData类只存储固定的测试值(比如Guid、固定名称、文本内容等原子数据),绝对不要存储实体类实例。
- 每个测试用例需要用到实体时,当场实例化全新的对象,再赋值属性,示例:
[Test] public async Task ReplaceItemTags_EmptyFirst() { // 每次测试都new全新的实体实例,不存在复用污染 var testTag = new Tag { Id = TestData.TagBId, Name = TestData.TagBName }; var testItem = new Item { Id = TestData.AlexItemAId, Name = TestData.AlexItemAName, Tags = new List<Tag>() }; var originalTags = testItem.Tags; await _context.Items.AddAsync(testItem); await _unitOfWork.CompleteAsync(); var result = await _ItemRepository.ReplaceItemTags(testItem.Id, new List<Tag>() { testTag }); await _unitOfWork.CompleteAsync(); Assert.NotNull(result); Assert.AreNotEqual(originalTags, result?.Tags); Assert.AreEqual(1, result?.Tags?.Count); Assert.Contains(testTag, result?.Tags?.ToList()); } - 如果需要频繁创建测试实体,可以编写独立的实体工厂方法,每次调用返回全新的实例,不要复用缓存的实体对象。
额外注意:测试带导航属性的实体逻辑时,尽量避免直接跨上下文传递实体实例,优先通过Id查询关联实体,或者显式在当前上下文Attached实体后再操作,减少EF Core状态判断错误的概率。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

