集成测试共享DbContext时用户更新变更未同步的问题排查
我在编写集成测试时使用TestContainer包(SQL Server),并通过BaseWebApplicationTest抽象类共享HttpClient和AppDbContext等属性。
我的WebApplicationFactory实现如下:
public class IntegrationTestWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>, IAsyncLifetime where TProgram : class { private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-latest") .Build(); protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.RemoveAll<DbContextOptions<AppDbContext>>(); services.AddSqlServer<AppDbContext>(_msSqlContainer.GetConnectionString()); }); } public Task InitializeAsync() => _msSqlContainer.StartAsync(); public new Task DisposeAsync() => _msSqlContainer.StopAsync(); }
共享属性的抽象类:
public abstract class BaseWebApplicationTest { private readonly IServiceScope _scope; protected readonly HttpClient _httpClient; protected readonly AppDbContext _context; protected BaseWebApplicationTest(IntegrationTestWebApplicationFactory<Program> factory) { _scope = factory.Services.CreateScope(); _context = _scope.ServiceProvider.GetRequiredService<AppDbContext>(); _httpClient = factory.CreateClient(); } }
创建用户的测试用例可正常运行,但执行用户更新测试时出现问题:调用更新接口后,测试中的AppDbContext无法获取到变更数据,查询到的仍是旧值。更新测试用例及报错信息如下:
[Fact] public async Task Update_Should_Return200StatusCode_WhenRequestDataIsValid() { // Arrange var role = new Role() { Name = "admin1x", Description = "This is admin user" }; var user = new User() { FirstName = "First Name", LastName = "Last Name", Email = "email@example2.com", Password = "Pass123@", Username = "user_name3", Role = role }; await _context.Roles.AddAsync(role); await _context.Users.AddAsync(user); await _context.SaveChangesAsync(); CreateUserRequest request = new( "Name changed", "Last Name changed", "user_changed", "example_changed@email.com", "PassChanged123$", RoleId: role.Id); // Act HttpResponseMessage response = await _httpClient.PutAsJsonAsync( $"/api/user/{user.Id}", request); // Assert response.EnsureSuccessStatusCode(); response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK); Guid id = await response.Content.ReadFromJsonAsync<Guid>(); id.Should().NotBeEmpty(); var userChanged = await _context.Users.FindAsync(id); userChanged?.FirstName.Should().Be(request.FirstName); userChanged?.LastName.Should().Be(request.LastName); userChanged?.Username.Should().Be(request.UserName); }
报错信息:
Expected userChanged?.FirstName to be "Name changed" with a length of 12, but "First Name" has a length of 10, differs near "Fir" (index 0).
用户更新服务本身正常,调用_context.Entry(userChanged).Reload();后测试即可通过。请问这是什么原因,该如何解决?
原因
EF Core的DbContext自带一级缓存(上下文缓存),测试代码中先通过_context.Users.AddAsync(user)并保存后,这个user对象已经被缓存在当前_context实例中。
当你通过HttpClient调用更新接口时,接口内部会使用独立的新DbContext实例(Web请求会创建新的请求作用域,从容器中获取全新的DbContext)来更新数据库数据,但测试代码中的_context无法感知数据库的变化。此时调用_context.Users.FindAsync(id),EF Core会直接返回缓存里的旧对象,不会去数据库重新查询。
解决方案
有三种常用的解决方式:
方式1:强制重新加载实体
直接调用Reload()方法,强制从数据库刷新指定实体的最新数据:
var userChanged = await _context.Users.FindAsync(id); _context.Entry(userChanged).Reload(); // 从数据库重新加载实体 userChanged?.FirstName.Should().Be(request.FirstName);
方式2:创建新的DbContext实例
利用现有作用域创建新的DbContext,绕过旧上下文的缓存:
using var newScope = _scope.ServiceProvider.CreateScope(); var newContext = newScope.ServiceProvider.GetRequiredService<AppDbContext>(); var userChanged = await newContext.Users.FindAsync(id); userChanged?.FirstName.Should().Be(request.FirstName);
方式3:重构抽象类,按需获取新上下文(推荐)
修改BaseWebApplicationTest,避免长期持有同一个DbContext实例,而是提供方法按需创建新的上下文:
public abstract class BaseWebApplicationTest { protected readonly IntegrationTestWebApplicationFactory<Program> _factory; protected readonly HttpClient _httpClient; protected BaseWebApplicationTest(IntegrationTestWebApplicationFactory<Program> factory) { _factory = factory; _httpClient = factory.CreateClient(); } // 提供获取全新上下文的方法 protected AppDbContext GetNewDbContext() { var scope = _factory.Services.CreateScope(); return scope.ServiceProvider.GetRequiredService<AppDbContext>(); } }
测试中使用新上下文查询:
var userChanged = await GetNewDbContext().Users.FindAsync(id); userChanged?.FirstName.Should().Be(request.FirstName);
这种方式每次查询都使用无缓存的新DbContext,能稳定获取数据库最新数据,适合频繁验证更新结果的测试场景。
内容的提问来源于stack exchange,提问作者Skeytor

