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

集成Testcontainers与WebApplicationFactory时DbContext查询异常

问题:ASP.NET Core Web API(.NET 8)集成测试中,Testcontainers+SQL Server保存实体后查询返回空集合

我正在为ASP.NET Core Web API(.NET 8)配置Testcontainers + SQL Server编写xUnit集成测试,遇到了一个异常情况:实体保存成功(无异常抛出,SQL客户端可查看到数据),但紧接着的查询却返回空集合。

WebApplicationFactory实现代码

public class IntegrationTestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly MsSqlContainer _dbContainer = new MsSqlBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
        .WithPassword("Strong_password_123!")
        .Build();

    public Task InitializeAsync() => _dbContainer.StartAsync();

    public new Task DisposeAsync() => _dbContainer.StopAsync();

    protected override void ConfigureWebHost(IWebHostBuilder builder) =>
        builder.ConfigureTestServices(services =>
        {
            Type descriptorType = typeof(DbContextOptions<SpendCentricContext>);

            ServiceDescriptor? descriptor = services.SingleOrDefault(s => s.ServiceType == descriptorType);

            if (descriptor is not null)
                services.Remove(descriptor);

            services.AddTransient<ITimeProvider, MockTimeProvider>();
            services.AddScoped<ITenantService, MockTenantService>();
            services.AddScoped<ICorrelationIdProvider, MockCorrelationIdProvider>();

            string? connString = _dbContainer.GetConnectionString();
            services.AddDbContext<SpendCentricContext>(options => options.UseSqlServer(connString));
        });
}

容器启动正常,数据库连接无问题。

集成测试基类

public abstract class IntegrationTestBase : IClassFixture<IntegrationTestWebAppFactory>, IDisposable
{
    protected readonly IServiceScope scope;
    private readonly SpendCentricContext _dbContext;

    protected IntegrationTestBase(IntegrationTestWebAppFactory factory)
    {
        scope = factory.Services.CreateScope();

        _dbContext = scope.ServiceProvider.GetRequiredService<SpendCentricContext>();
        _dbContext.Database.EnsureCreated();
    }

    public void Dispose()
    {
        _dbContext?.Dispose();
        scope?.Dispose();
    }
}

测试数据填充代码(问题出现位置)

private async Task SeedTestData()
{
    var account = new Account
    {
        Id = Guid.Parse("faa3bfb3-42d7-4796-e701-08d9aff85e7f"),
        Name = "Banca Intesa",
        Description = "Intesa",
        AccountNumber = "",
        AccountCurrency = new Currency("EUR"),
        UserId = Guid.Parse("f9caa978-2ccc-4837-945f-dbc45ae83f72"),
        AccountType = AccountType.Bank,
        IsDefault = true,
        DateCreated = new DateTime(2022, 1, 1),
        DateUpdated = new DateTime(2022, 1, 1),
        IsDeleted = false,
        Transactions = new HashSet<Transaction>()
    };

    IAccountRepository accountRepository = scope.ServiceProvider
        .GetRequiredService<IAccountRepository>();

    var result = await accountRepository.AddAccount(account);
    var current = await accountRepository.GetAccounts();
}

补充仓储实现代码

public sealed class AccountRepository(SpendCentricContext dbContext, ILogger logger) : IAccountRepository
{
    public async Task<Account> AddAccount(Account account)
    {
        EntityEntry<Account> accountEntry = await dbContext.Accounts.AddAsync(account);
        return await dbContext.SaveChangesAsync() == 1
            ? accountEntry.Entity
            : throw new ArgumentException(nameof(account));
    }

    public async Task<Account> GetAccount(Guid id, bool includeTransactions = false)
    {
        try
        {
            IQueryable<Account> account = dbContext.Accounts.AsNoTracking();
            if (includeTransactions)
                account.Include(x => x.Transactions);

            return await account.SingleAsync(x => x.Id == id);
        }
        catch (Exception ex)
        {
            logger.Error(ex, "Error getting account with id: {Id}", id);
            throw;
        }
    }

    // 根据描述补充的GetAccounts实现
    public async Task<List<Account>> GetAccounts()
    {
        return await dbContext.Accounts.AsNoTracking().ToListAsync();
    }
}

核心问题现象:

  • result不为空,SaveChangesAsync未抛出任何异常
  • 用SQL客户端连接容器内的数据库,能看到实体已成功写入
  • 但紧接着调用GetAccounts返回的是空集合

可能的原因及解决方案

1. DbContext实例不一致

如果AddAccount和GetAccounts使用的是不同的DbContext实例,EF Core的变更追踪机制可能导致查询无法获取刚写入的数据(即使使用了AsNoTracking)。比如仓储被错误注册为Transient,或者在获取仓储时意外创建了新的Scope。

解决办法:

  • 确保整个测试流程使用同一个Scope内的DbContext和仓储实例
  • 在SaveChangesAsync后,调用dbContext.ChangeTracker.Clear()强制后续查询从数据库拉取最新数据

2. 全局查询过滤器影响

检查SpendCentricContext中是否为Account实体配置了全局查询过滤器(比如软删除过滤),即使实体的IsDeleted设为false,过滤器逻辑可能存在错误导致数据被过滤。

解决办法:

  • 查看OnModelCreating方法中的配置,确认全局过滤器规则正确
  • 测试时可以临时禁用过滤器:dbContext.Accounts.IgnoreQueryFilters().ToListAsync()

3. 事务未提交

如果测试代码中存在未提交的事务,即使SaveChangesAsync执行成功,数据仍处于事务中未持久化到数据库,导致查询无法获取。

解决办法:

  • 检查测试基类或其他代码是否开启了事务但未提交
  • 在保存数据后显式提交事务(如果手动管理了事务)

4. AsNoTracking的潜在问题

虽然AsNoTracking会绕过变更追踪,但如果当前DbContext的追踪缓存中存在该实体,仍可能出现查询异常。可以尝试在保存后将实体从追踪缓存中移除。

解决办法:

  • 在SaveChangesAsync后添加:dbContext.Entry(account).State = EntityState.Detached

5. SQL Server容器写入延迟

极少数情况下,Testcontainers中的SQL Server可能存在写入后的短暂同步延迟,导致查询时数据还未落地。

排查手段:

  • 在AddAccount后添加短暂等待:await Task.Delay(100),测试是否能获取数据(不建议作为正式解决方案,仅用于排查)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 13:07:32