You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core InMemory数据库测试 串行执行抛出DbUpdateConcurrencyException

问题根因

你的猜测方向完全正确,核心问题是静态测试实体实例复用+DbContext生命周期设计缺陷共同导致的:

  1. 静态TestData类持有的是全局共享的实体实例,即使上一个测试执行完调用了ChangeTracker.Clear()、分离了当前上下文的所有追踪条目、甚至删除了内存数据库,这些静态实体实例本身的属性值、导航属性引用、EF Core之前写入的影子属性(比如主键值、外键关联标记)都已经被修改,不会被重置。下一个测试把这些被其他上下文追踪过的实例Add到新上下文时,EF Core扫描到实例带有非默认的主键值,会错误将关联的Tag等实体标记为Modified状态,但全新的内存数据库里根本没有对应主键的记录,执行SaveChanges时就会抛出你看到的DbUpdateConcurrencyException。
  2. 现有测试基类在构造函数中初始化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 08:21:25