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

集成测试中内存数据库数据跨测试运行持久化问题排查

集成测试中InMemory数据库数据残留问题排查与解决

核心问题原因

  • EF Core的InMemory数据库是进程内共享的:只要数据库名称相同,即使创建不同的DbContext实例,也会连接到同一个内存数据库实例。你固定用"TestDb"作为名称,导致多次测试复用同一数据库,之前的测试数据残留。
  • 测试进程复用:Visual Studio的测试运行器默认会复用测试进程,即使单个测试结束,进程不关闭的话,内存数据库不会被销毁,这就是你重启VS才会“重置”的原因(其实是进程被杀死了)。

你的代码里的问题

  1. 固定数据库名称,导致跨测试共享数据
  2. IntegrationTest类未实现IDisposable,没有正确释放WebApplicationFactory资源
  3. 多余的ServiceCollection创建:你在ConfigureServices里额外创建了一个ServiceProvider,这完全没必要,反而可能导致上下文实例不一致

解决方案

1. 使用唯一的内存数据库名称

每次创建测试上下文时,生成一个唯一的数据库名(比如用Guid),确保每个测试用例/测试类都有独立的数据库:

options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}");

2. 实现IDisposable清理资源

让IntegrationTest类实现IDisposable,在Dispose方法中销毁_appFactory和_testClient,确保测试结束后释放相关资源:

public class IntegrationTest : IDisposable
{
    protected readonly HttpClient _testClient;
    private readonly WebApplicationFactory<Program> _appFactory;
    private bool _disposed = false;

    protected IntegrationTest()
    {
        _appFactory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // 移除原有DbContextOptions注册(比RemoveAll更彻底)
                    var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                    if (descriptor != null)
                        services.Remove(descriptor);

                    // 注册带唯一名称的内存数据库
                    services.AddDbContext<AppDbContext>(options =>
                    {
                        options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}");
                    });

                    // 初始化数据库(用内置的ServiceProvider)
                    using var scope = services.BuildServiceProvider().CreateScope();
                    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                    db.Database.EnsureCreated();
                });
            });
        _testClient = _appFactory.CreateClient();
    }

    protected async Task AuthenticateAsync()
    {
        _testClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", await GetBearerTokenAsync());
    }

    private async Task<string> GetBearerTokenAsync()
    {
        // 动态生成唯一邮箱,避免重复注册失败
        var uniqueEmail = $"test_{Guid.NewGuid()}@gmail.com";
        var registerResponse = await _testClient.PostAsJsonAsync("/register", new
        {
            email = uniqueEmail,
            password = "Passw0rd."
        });
        registerResponse.EnsureSuccessStatusCode();
        
        // 补充后续获取token的逻辑
        // ...
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            _testClient?.Dispose();
            _appFactory?.Dispose();
        }

        _disposed = true;
    }
}

3. 修复硬编码邮箱问题

你在GetBearerTokenAsync里硬编码了固定邮箱,即使换了数据库,同一个测试进程里多次调用注册接口也会失败。改成动态生成唯一邮箱就能彻底解决这个问题。

补充说明

  • 内存数据库不会在关机后保留:你觉得关机后数据还在,是因为测试进程没有被完全杀死,VS测试运行器会复用进程。重启VS会杀死旧进程,内存被释放,所以数据才会“重置”。
  • services.RemoveAll(typeof(AppDbContext))不够彻底:EF Core是通过DbContextOptions<AppDbContext>来配置DbContext的,只移除AppDbContext本身可能无法完全清除原有配置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 12:23:11