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

.NET API集成测试首次执行后数据库被清空问题求助

.NET API集成测试数据库数据丢失问题排查与解决

问题描述

我在为.NET API编写首个集成测试时遇到异常:首次测试执行后数据库被清空。原本预期IClassFixture能保证测试间数据持久,但初始数据库中的用户在login测试执行后消失,运行CreateNote测试时,AcquireToken方法内的Assert.True(result.IsSuccessStatusCode)断言失败,返回False。

相关代码

CustomWebApplicationFactory

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services => 
        {
            var dbContextDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<DataContext>));

            if(dbContextDescriptor != null) services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbConnection)
            );

            if(dbConnectionDescriptor != null) services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<DataContext>(
                (container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                }
            );
        });

        builder.ConfigureTestServices(services => 
        {
            //services.AddSingleton<ITokenService, TokenService>();
            services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        });

        builder.UseEnvironment("Development");
    }
}

HttpClientTest

public abstract class HttpClientTest : IClassFixture<CustomWebApplicationFactory<Program>> , IDisposable
{
    
    protected readonly HttpClient _httpClient;
    protected readonly JsonSerializerOptions _serializerOptions;
    protected readonly CustomWebApplicationFactory<Program> _factory;


    public HttpClientTest(CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _httpClient = factory.CreateClient(
            new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }
        );

        _serializerOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        };

#pragma warning disable CS4014
        SetupTestData();
#pragma warning restore CS4014
    }

    private async Task SetupTestData()
    {
        using var scope = _factory.Services.CreateScope();
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<DataContext>();

        await Utilities.InitializeDbForTests(db);
    }

    protected async Task<string> AcquireToken()
    {
        var d = new LoginDto { Email = "jan@example.com", Password = "Pa$$w0rd" };
        var content = JsonSerializer.Serialize(d);

        var result = await DoPost("/api/patients/account/login", content);
        Assert.True(result.IsSuccessStatusCode);
        Assert.Equal("application/json", result.Content.Headers.ContentType!.MediaType);

        var resultJson = await result.Content.ReadAsStringAsync();
        var resultObj = JsonSerializer.Deserialize<PatientDto>(resultJson, _serializerOptions);

        Assert.NotNull(resultObj);
        Assert.NotNull(resultObj.Token);

        return resultObj.Token;
    }

    protected async Task<HttpResponseMessage> DoGet(string url)
    {
        return await _httpClient.GetAsync(url); //.ConfigureAwait(false);
    }

    protected async Task<HttpResponseMessage> DoGetAuthenticated(string url, string token)
    {
        var client = _factory.CreateClient();

        if (token != null)
        {
            client.DefaultRequestHeaders.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }

        
        var result = await client.GetAsync(url); //ConfigureAwait(false);
        return result;
    }

    protected async Task<HttpResponseMessage> DoPostAuthenticated(
        string url,
        string token,
        string content
    )
    {
        var client = _factory.CreateClient();

        if (token != null)
        {
            client.DefaultRequestHeaders.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }

        return await client
            .PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"))
            .ConfigureAwait(false);
    }

    protected async Task<HttpResponseMessage> DoPost(string url, string content)
    {
        return await _httpClient
            .PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"))
            .ConfigureAwait(false);
    }

    protected async Task<HttpResponseMessage> DoPutAuthenticated(string url, string token, string content)
    {
        var client = _factory.CreateClient();
        if (token != null)
        {
            client.DefaultRequestHeaders.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }
        return await client
            .PutAsync(url, new StringContent(content, Encoding.UTF8, "application/json"))
            .ConfigureAwait(false);
    }

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

Tests

public class PatientsControllerIntegrationTests : HttpClientTest
{
    public PatientsControllerIntegrationTests(CustomWebApplicationFactory<Program> factory) : base(factory)
    {
    }

    [Fact]
    public async Task login()
    {
        var d = new LoginDto { Email = "jan@example.com", Password = "Pa$$w0rd" };
        var content = JsonSerializer.Serialize(d);

        var result = await DoPost("/api/patients/account/login", content);
        Assert.True(result.IsSuccessStatusCode);
        Assert.Equal("application/json", result.Content.Headers.ContentType!.MediaType);
    }


    [Fact]
    public async Task CreateNote()
    {
        var n = new NoteDto{Description = "Test", Value = 1};
        var content = JsonSerializer.Serialize(n);
        var token = await AcquireToken();

        var result = await DoPostAuthenticated("/api/notes/1", token, content);
        Assert.True(result.IsSuccessStatusCode);
    }
}

错误信息

Assert.True() Failure
Expected: True
Actual:   False
  调用栈:
     at APITests.HttpClientTest.AcquireToken() in C:\Users\karol\Desktop\praca inż\APITests\HttpClientTest.cs:line 50
   at APITests.PatientsControllerIntegrationTests.CreateNote() in C:\Users\karol\Desktop\praca inż\APITests\PatientsControllerIntegrationTests.cs:line 40

问题根源与解决方案

1. 异步初始化未等待导致数据未就绪

构造函数中调用SetupTestData()时未等待异步操作完成(通过#pragma warning disable CS4014忽略了警告),导致测试可能在数据初始化完成前就开始执行,或者初始化操作被中断。

修复方式:改用xUnit的IAsyncLifetime接口处理异步初始化,xUnit会等待InitializeAsync完成后再执行测试:

public abstract class HttpClientTest : IClassFixture<CustomWebApplicationFactory<Program>>, IAsyncLifetime, IDisposable
{
    // 原有字段和构造函数保持不变

    public async Task InitializeAsync()
    {
        await SetupTestData();
    }

    // 原有SetupTestData及其他方法保持不变

    public Task DisposeAsync()
    {
        Dispose();
        return Task.CompletedTask;
    }
}

2. 测试并行执行导致数据竞争

xUnit默认会并行执行测试,多个测试同时操作共享的内存数据库,可能出现一个测试清空/修改数据、另一个测试读取到异常数据的情况。

修复方式:在测试项目根目录添加xunit.runner.json文件,禁止并行执行:

{
  "parallelizeTestCollections": false,
  "parallelizeAssembly": false
}

并将该文件的「复制到输出目录」设置为「如果较新则复制」。

3. 调试定位具体错误

在AcquireToken方法中添加日志,查看登录请求的具体响应内容,明确失败原因(比如用户不存在、密码错误等):

protected async Task<string> AcquireToken()
{
    var d = new LoginDto { Email = "jan@example.com", Password = "Pa$$w0rd" };
    var content = JsonSerializer.Serialize(d);

    var result = await DoPost("/api/patients/account/login", content);
    // 添加以下代码查看错误详情
    if (!result.IsSuccessStatusCode)
    {
        var errorContent = await result.Content.ReadAsStringAsync();
        Console.WriteLine($"登录失败,状态码:{result.StatusCode},内容:{errorContent}");
    }
    Assert.True(result.IsSuccessStatusCode);
    // 原有代码保持不变
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 21:14:56