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

EF Core内存数据库单元测试重复键问题及代码重构咨询

问题解答与代码重构

1. 错误产生的原因

这个问题的核心在于两点:

  • EF Core内存数据库的共享特性:同名的内存数据库本质是同一个实例,所有使用该名称的DbContext都会连接到同一个数据库。
  • xUnit默认并行执行测试:当两个测试同时运行时,它们会同时往同一个内存数据库插入主键为1、2、3的Movie记录——而数据库的主键约束不允许重复键值,因此抛出An item with the same key has already been added的错误。

单独运行测试时,数据库是独占的,不会有冲突;使用不同数据库名称时,每个测试对应独立的数据库实例,自然也不会有问题。

2. 代码重构方案

我们可以通过消除重复代码和避免测试间数据污染两个方向来重构,下面提供两种实用的方案:

方案一:为每个测试生成唯一内存数据库(推荐,简单无依赖)

这种方式让每个测试拥有独立的数据库实例,彻底避免并行冲突,同时提取重复的初始化代码到辅助方法,让测试更简洁:

public class MovieRepositoryTest
{
    // 生成唯一的内存数据库名称,确保每个测试用独立数据库
    private DbContextOptions<MovieDbContext> CreateUniqueInMemoryDbOptions()
    {
        return new DbContextOptionsBuilder<MovieDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
    }

    // 提取重复的测试数据初始化逻辑
    private void SeedTestMovies(MovieDbContext context)
    {
        context.Movies.AddRange(new List<Movie>
        {
            new Movie { Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" },
            new Movie { Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action" },
            new Movie { Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action" }
        });
        context.SaveChanges();
    }

    [Fact]
    public void GetAll_WhenCalled_ReturnsAllItems()
    {
        var options = CreateUniqueInMemoryDbOptions();

        // 初始化测试数据
        using (var context = new MovieDbContext(options))
        {
            SeedTestMovies(context);
        }

        // 执行测试逻辑
        using (var context = new MovieDbContext(options))
        {
            var sut = new MovieRepository(context);
            var movies = sut.GetAll();
            Assert.Equal(3, movies.Count());
        }
    }

    [Fact]
    public void Search_ValidTitlePassed_ReturnsOneMovie()
    {
        var filters = new MovieFilters { Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" };
        var options = CreateUniqueInMemoryDbOptions();

        // 初始化测试数据
        using (var context = new MovieDbContext(options))
        {
            SeedTestMovies(context);
        }

        // 执行测试逻辑
        using (var context = new MovieDbContext(options))
        {
            var sut = new MovieRepository(context);
            var movies = sut.Search(filters);
            Assert.Single(movies);
        }
    }
}

方案二:使用xUnit Fixture共享数据库(适合初始化成本高的场景)

如果测试数据初始化成本较高,可以用IClassFixture共享数据库实例,但需要注意测试间的数据隔离,并且通过集合标记避免并行冲突:

第一步:创建Fixture类(负责数据库初始化与清理)

public class MovieDbFixture : IDisposable
{
    public DbContextOptions<MovieDbContext> DbOptions { get; }

    public MovieDbFixture()
    {
        // 创建共享的内存数据库
        DbOptions = new DbContextOptionsBuilder<MovieDbContext>()
            .UseInMemoryDatabase(databaseName: "SharedMovieTestDb")
            .Options;

        // 初始化基础测试数据
        using (var context = new MovieDbContext(DbOptions))
        {
            SeedTestMovies(context);
        }
    }

    private void SeedTestMovies(MovieDbContext context)
    {
        context.Movies.AddRange(new List<Movie>
        {
            new Movie { Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" },
            new Movie { Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action" },
            new Movie { Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action" }
        });
        context.SaveChanges();
    }

    // 每个测试后清理数据,避免污染后续测试
    public void Dispose()
    {
        using (var context = new MovieDbContext(DbOptions))
        {
            context.Movies.RemoveRange(context.Movies);
            context.SaveChanges();
        }
    }
}

// 定义测试集合,确保集合内的测试串行执行
[CollectionDefinition("MovieTestCollection")]
public class MovieTestCollection : ICollectionFixture<MovieDbFixture>
{
    // 空类,仅用于标记集合
}

第二步:重构测试类使用Fixture

[Collection("MovieTestCollection")]
public class MovieRepositoryTest
{
    private readonly MovieDbFixture _fixture;

    public MovieRepositoryTest(MovieDbFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public void GetAll_WhenCalled_ReturnsAllItems()
    {
        using (var context = new MovieDbContext(_fixture.DbOptions))
        {
            var sut = new MovieRepository(context);
            var movies = sut.GetAll();
            Assert.Equal(3, movies.Count());
        }
    }

    [Fact]
    public void Search_ValidTitlePassed_ReturnsOneMovie()
    {
        var filters = new MovieFilters { Title = "Movie 1", YearOfRelease = 2018, Genre = "Action" };

        using (var context = new MovieDbContext(_fixture.DbOptions))
        {
            var sut = new MovieRepository(context);
            var movies = sut.Search(filters);
            Assert.Single(movies);
        }
    }
}

内容的提问来源于stack exchange,提问作者MarcosF8

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:18:21