XUnit+SQL Server集成测试:事务导致数据无法写入的问题
解决XUnit集成测试中事务内数据无法被API读取的问题
问题根源
你开启的事务是当前DbContext实例的本地事务,而WebApplicationFactory创建的测试客户端会为每个API请求创建全新的DbContext实例——这个新实例不在你手动开启的事务范围内,因此无法读取到事务中未提交的数据。这就是为什么移除事务代码后测试能通过(数据直接提交到数据库,新DbContext能读到),但开启事务后测试失败的原因。
可行解决方案
方案1:共享DbContext与事务(推荐)
让API请求使用的DbContext和你测试代码中的DbContext是同一个实例,这样它能直接看到事务中的未提交数据,测试结束后回滚事务即可保证数据库干净。
修改后的测试代码示例:
[Fact] public async Task GetAsync_ReturnsTaskOfRoleModelList() { using var context = _factory.CreateContext(); _rolesRepository = () => _factory.CreateRoleRepository(context); // 开启事务 using var transaction = await context.Database.BeginTransactionAsync(); try { // 添加测试数据(假设CreateAsync内部已调用SaveChangesAsync) await _rolesRepository().CreateAsync(new RoleModel { Id = Guid.NewGuid(), Name = "Role A", Description = "Role A Description", CreatedDate = DateTime.Now, UpdatedDate = DateTime.Now, Deleted = false, }); // 创建共享当前DbContext的测试客户端 var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // 移除原有DbContext配置,替换为当前带事务的实例 var existingDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<YourDbContext>) ); if (existingDescriptor != null) { services.Remove(existingDescriptor); } // 注入当前上下文实例 services.AddScoped<YourDbContext>(_ => context); }); }).CreateClient(); // 发送API请求 var response = await client.GetFromJsonAsync<List<RoleModel>>("/api/roles"); // 断言验证 response.ShouldNotBeNull(); response.ShouldBeOfType<List<RoleModel>>(); response.Count.ShouldBeGreaterThan(0); } finally { // 回滚事务,彻底清除测试数据 await transaction.RollbackAsync(); context.ChangeTracker.Clear(); } }
方案2:使用TransactionScope跨上下文共享事务
利用TransactionScope创建分布式事务范围,让测试代码的DbContext和API的DbContext都加入同一个事务,测试结束后自动回滚。
注意:需要确保SQL Server启用分布式事务,且代码中使用兼容的异步配置:
[Fact] public async Task GetAsync_ReturnsTaskOfRoleModelList() { // 配置TransactionScope支持异步 using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); using var context = _factory.CreateContext(); _rolesRepository = () => _factory.CreateRoleRepository(context); try { await _rolesRepository().CreateAsync(new RoleModel { Id = Guid.NewGuid(), Name = "Role A", Description = "Role A Description", CreatedDate = DateTime.Now, UpdatedDate = DateTime.Now, Deleted = false, }); // 正常使用测试客户端,它的DbContext会自动加入当前事务 var response = await _client.GetFromJsonAsync<List<RoleModel>>("/api/roles"); // 断言验证 response.ShouldNotBeNull(); response.ShouldBeOfType<List<RoleModel>>(); response.Count.ShouldBeGreaterThan(0); } finally { // 不调用Complete(),事务自动回滚 scope.Dispose(); context.ChangeTracker.Clear(); } }
方案3:提交事务后手动清理数据(不推荐)
先提交事务让API读取到数据,测试完成后手动删除测试数据。这种方式风险较高,若测试中途失败可能导致数据残留:
[Fact] public async Task GetAsync_ReturnsTaskOfRoleModelList() { using var context = _factory.CreateContext(); _rolesRepository = () => _factory.CreateRoleRepository(context); var testRole = new RoleModel { Id = Guid.NewGuid(), Name = "Role A", Description = "Role A Description", CreatedDate = DateTime.Now, UpdatedDate = DateTime.Now, Deleted = false, }; try { await _rolesRepository().CreateAsync(testRole); // 提交事务让API能读到数据 await context.Database.CommitTransactionAsync(); var response = await _client.GetFromJsonAsync<List<RoleModel>>("/api/roles"); // 断言验证 response.ShouldNotBeNull(); response.ShouldBeOfType<List<RoleModel>>(); response.Count.ShouldBeGreaterThan(0); } finally { // 手动删除测试数据 context.Roles.Remove(testRole); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); } }
总结
优先选择方案1或方案2,它们能在保证API读取到测试数据的同时,通过事务回滚自动清理数据库,避免手动清理的繁琐和数据残留风险。
内容的提问来源于stack exchange,提问作者Samu R
相关产品推荐
相关产品推荐

