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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 22:21:01