ASP.NET Core 7移除Startup.cs后集成测试返回404问题求助
我原本的asp.net core 7 web api应用使用Program.cs和Startup.cs运行,基于CustomWebApplicationFactory<TStartup>编写的**集成测试(Integration Tests)**均运行正常。
现在我决定移除Startup.cs,将Startup.cs中的所有逻辑迁移到Program.cs中。我的Program.cs代码如下:
try { //从appSettings读取配置 var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); //初始化日志器 Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(config) .CreateLogger(); Log.Information($"Starting {typeof(Program).Assembly.FullName}"); var builder = WebApplication.CreateBuilder(); builder.Host.UseSerilog();//使用Serilog替代默认的.NET日志器 builder.WebHost.ConfigureKestrel(options => { // 设置属性并调用options的方法 options.AddServerHeader = false; }); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("role", ClaimTypes.Role); builder.Services.AddHttpContextAccessor() .AddApiClients(builder.Configuration) .AddApplicationServices(builder.Configuration) .AddMemoryCache() .AddResponseCompression() .AddApiControllersAndBehavior() .AddApiVersion() .AddApiAuthenticationAndAuthorization(builder.Configuration) .AddSwagger(builder.Configuration) .AddApplicationServices() .AddCors(options => { options.AddPolicy("AppClients", policyBuilder => policyBuilder.WithOrigins(builder.Configuration.GetValue<string>("WebClient")) .AllowAnyHeader() .AllowAnyMethod()); }) //.AddHttpLogging(options => //{ // options.LoggingFields = HttpLoggingFields.All; //}) .AddFeatureManagement() .UseDisabledFeaturesHandler(new DisabledFeatureHandler()); var consoleLogging = new ConsoleLogging(builder.Configuration.GetValue<bool>("EnableConsoleLogging")); builder.Services.AddSingleton(consoleLogging); var commandsConnectionString = new CommandConnectionString(builder.Configuration.GetConnectionString("CommandsConnectionString")); builder.Services.AddSingleton(commandsConnectionString); var queriesConnectionString = new QueryConnectionString(builder.Configuration.GetConnectionString("QueriesConnectionString")); builder.Services.AddSingleton(queriesConnectionString); if (builder.Environment.IsDevelopment()) { //builder.Services.AddScoped<BaseReadContext, AppInMemoryReadContext>(); //builder.Services.AddScoped<BaseContext, AppInMemoryContext>(); builder.Services.AddScoped<BaseReadContext, AppSqlServerReadContext>(); builder.Services.AddScoped<BaseContext, AppSqlServerContext>(); builder.Services.AddMiniProfilerServices(); } else { builder.Services.AddScoped<BaseReadContext, AppSqlServerReadContext>(); builder.Services.AddScoped<BaseContext, AppSqlServerContext>(); } var app = builder.Build(); app.UseMiddleware<ExceptionHandler>() //.UseHttpLogging() .UseSecurityHeaders(SecurityHeadersDefinitions.GetHeaderPolicyCollection(builder.Environment.IsDevelopment())) .UseHttpsRedirection() .UseResponseCompression(); if (builder.Environment.IsDevelopment()) { app.UseMiniProfiler() .UseSwagger() .UseSwaggerUI(options => { foreach (var description in app.Services.GetRequiredService<IApiVersionDescriptionProvider>().ApiVersionDescriptions) { options.SwaggerEndpoint( $"swagger/AppOpenAPISpecification{description.GroupName}/swagger.json", $"App API - {description.GroupName.ToUpperInvariant()}"); } options.OAuthClientId("appswaggerclient"); options.OAuthAppName("App API"); options.OAuthUsePkce(); options.RoutePrefix = string.Empty; options.DefaultModelExpandDepth(2); options.DefaultModelRendering(ModelRendering.Model); options.DocExpansion(DocExpansion.None); options.DisplayRequestDuration(); options.EnableValidator(); options.EnableFilter(); options.EnableDeepLinking(); options.DisplayOperationId(); }); } app.UseRouting() .UseCors("AppClients") .UseAuthentication() .UseAuthorization() .UseRequestLocalization(options => { var supportedCultures = new[] { "en", "en-IN", "en-US" }; options.SetDefaultCulture("en-IN"); options.AddSupportedCultures(supportedCultures); options.AddSupportedUICultures(supportedCultures); options.ApplyCurrentCultureToResponseHeaders = true; }) .UseEndpoints(endpoints => { endpoints.MapControllers(); }); await app.RunAsync(); } catch (Exception ex) { Log.Fatal(ex, "应用启动失败。"); } finally { Log.CloseAndFlush(); } /// <summary> /// 添加此部分使功能测试可编译 /// </summary> public partial class Program { }
以下是我的CustomWebApplicationFactory<TProgram>代码:
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { var projectDir = Directory.GetCurrentDirectory(); builder.ConfigureAppConfiguration((context, conf) => { conf.AddJsonFile(Path.Combine(projectDir, "appsettings.Test.json")); }); builder.UseEnvironment("Testing"); builder.ConfigureTestServices(async services => { services.AddAuthentication("Test") .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { }); services.AddScoped(_ => AuthClaimsProvider.WithMasterClaims()); var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(BaseContext)); if (descriptor != null) { services.Remove(descriptor); } descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(BaseReadContext)); if (descriptor != null) { services.Remove(descriptor); } descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(ITenantService)); if (descriptor != null) { services.Remove(descriptor); services.AddTransient<ITenantService, TestTenantService>(); } var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" }; var connection = new SqliteConnection(connectionStringBuilder.ToString()); var dbContextOptions = new DbContextOptionsBuilder<AppSqliteInMemoryContext>() .UseSqlite(connection) .Options; services.AddScoped<BaseContext>(options => new AppSqliteInMemoryContext(dbContextOptions)); var dbContextReadOptions = new DbContextOptionsBuilder<AppSqliteInMemoryReadContext>() .UseSqlite(connection) .Options; services.AddScoped<BaseReadContext>(options => new AppSqliteInMemoryReadContext( dbContextReadOptions, options.GetRequiredService<ITenantService>())); await connection.CloseAsync(); var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<BaseContext>(); var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<Program>>>(); try { await db.Database.OpenConnectionAsync(); await db.Database.EnsureCreatedAsync(); await DatabaseHelper.InitialiseDbForTests(db); } catch (Exception ex) { logger.LogError(ex, $"为测试初始化数据库时出错。错误信息:{ex.Message}"); throw; } }); } }
以下是我的**集成测试(Integration Test)**代码:
public class GetByIdTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; public GetByIdTests(CustomWebApplicationFactory<Program> factory) { //factory.ClientOptions.BaseAddress = new Uri("https://localhost:44367"); _client = factory.CreateClient(new WebApplicationFactoryClientOptions { BaseAddress = new Uri("https://localhost:44367"), AllowAutoRedirect = false }); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test"); _client.DefaultRequestHeaders.Add("x-api-version", "1.0"); } [Fact] public async Task GetById_ReturnsExpectedResponse_ForMasterUser() { var id = Guid.Parse("6B4DFE8A-2FCB-4716-94ED-4D63BF9351C6"); using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/branches/{id}"); var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); } }
我已遵循官方文档中提到的所有步骤,但运行测试时持续收到404未找到错误。
以下是错误截图:

请协助我排查问题所在?
排查思路
1. 异步服务配置的问题
ConfigureTestServices不支持异步委托,你当前的代码用async修饰该方法,会导致数据库初始化等异步逻辑无法正确执行,进而引发服务注册不完整、控制器依赖注入失败,最终导致路由无法匹配。
修复方案:将异步初始化逻辑移到ConfigureTestServices之外,比如在WebHost启动后执行:
protected override void ConfigureWebHost(IWebHostBuilder builder) { // 原有配置代码... builder.ConfigureTestServices(services => { // 服务注册逻辑(移除async) // ... // 注册数据库初始化器 services.AddSingleton<DatabaseInitializer>(); }); // 自定义启动类执行异步初始化 builder.UseStartup<TestStartup>(); } private class TestStartup { public async void Configure(IApplicationBuilder app) { var initializer = app.ApplicationServices.GetRequiredService<DatabaseInitializer>(); await initializer.InitializeAsync(); } } private class DatabaseInitializer { public async Task InitializeAsync() { // 原有的数据库初始化逻辑 } }
2. 测试环境的API版本路由不匹配
检查BranchesController的路由特性:
- 如果控制器使用了
[Route("api/v{version:apiVersion}/branches")],则请求URL应该是/api/v1.0/branches/{id},而非当前的/api/branches/{id} - 确认
x-api-version请求头是否被API版本中间件正确识别,或者是否需要在URL中显式指定版本号
3. 手动指定BaseAddress无效
WebApplicationFactory会为测试应用分配随机端口,你手动指定的https://localhost:44367可能并非测试应用的实际监听地址,导致请求发送到错误位置。
修复方案:移除手动指定的BaseAddress,使用客户端默认地址:
_client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
4. 测试环境下的中间件顺序或配置缺失
检查Program.cs中是否有针对非Development环境的条件逻辑,导致路由相关中间件(如UseRouting、UseEndpoints)未正确执行,或者API版本、控制器注册逻辑在Testing环境下未生效。
5. 控制器依赖注入失败
如果控制器依赖的服务(如BaseContext、ITenantService)在测试环境下未正确注册,会导致控制器无法被实例化,路由系统会返回404。可以在测试中添加日志输出,确认控制器是否被正确注册。
内容的提问来源于stack exchange,提问作者fingers10

