Skip to content

Commit 11e36b6

Browse files
author
Fortinbra
committed
perf: tune stress profiles and add real DB mode for performance tests
- Reduce StressProfile from 100 to 30 req/s to prevent unbounded backlog - Add CalendarStressProfile (25 req/s) for calendar endpoint with 9 sequential queries - Add PERF_USE_REAL_DB env var toggle to run against real PostgreSQL - Resolve FirstAccountId from real DB when using real DB mode - Set 30s HttpClient timeout for stress scenarios
1 parent b8f7816 commit 11e36b6

File tree

5 files changed

+99
-13
lines changed

5 files changed

+99
-13
lines changed

tests/BudgetExperiment.Performance.Tests/Infrastructure/PerformanceWebApplicationFactory.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
namespace BudgetExperiment.Performance.Tests.Infrastructure;
1616

1717
/// <summary>
18-
/// Custom <see cref="WebApplicationFactory{TEntryPoint}"/> for performance tests
19-
/// using an in-memory database and auto-authentication.
18+
/// Custom <see cref="WebApplicationFactory{TEntryPoint}"/> for performance tests.
19+
/// By default uses an in-memory database. Set environment variable <c>PERF_USE_REAL_DB=true</c>
20+
/// to use the real PostgreSQL database from user secrets (same as debug sessions).
2021
/// </summary>
2122
public sealed class PerformanceWebApplicationFactory : WebApplicationFactory<Program>
2223
{
@@ -32,6 +33,12 @@ public sealed class PerformanceWebApplicationFactory : WebApplicationFactory<Pro
3233

3334
private readonly string _dbName = "PerfDb_" + Guid.NewGuid().ToString();
3435

36+
/// <summary>
37+
/// Gets a value indicating whether the factory is configured to use the real database.
38+
/// </summary>
39+
public static bool UseRealDb { get; } =
40+
string.Equals(Environment.GetEnvironmentVariable("PERF_USE_REAL_DB"), "true", StringComparison.OrdinalIgnoreCase);
41+
3542
/// <summary>
3643
/// Creates an <see cref="HttpClient"/> pre-configured with test authentication.
3744
/// </summary>
@@ -40,11 +47,38 @@ public HttpClient CreateApiClient()
4047
{
4148
var client = this.CreateClient();
4249
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("TestAuto", "authenticated");
50+
client.Timeout = TimeSpan.FromSeconds(30);
4351
return client;
4452
}
4553

4654
/// <inheritdoc />
4755
protected override void ConfigureWebHost(IWebHostBuilder builder)
56+
{
57+
if (UseRealDb)
58+
{
59+
ConfigureRealDb(builder);
60+
}
61+
else
62+
{
63+
this.ConfigureInMemoryDb(builder);
64+
}
65+
}
66+
67+
private static void ConfigureRealDb(IWebHostBuilder builder)
68+
{
69+
// Keep the real Npgsql DbContext from AddInfrastructure — only override auth
70+
builder.ConfigureServices(services =>
71+
{
72+
services.AddAuthentication(options =>
73+
{
74+
options.DefaultAuthenticateScheme = "TestAuto";
75+
options.DefaultChallengeScheme = "TestAuto";
76+
})
77+
.AddScheme<AuthenticationSchemeOptions, AutoAuthenticatingTestHandler>("TestAuto", options => { });
78+
});
79+
}
80+
81+
private void ConfigureInMemoryDb(IWebHostBuilder builder)
4882
{
4983
builder.ConfigureAppConfiguration((context, config) =>
5084
{

tests/BudgetExperiment.Performance.Tests/Infrastructure/TestDataSeeder.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using BudgetExperiment.Domain.Recurring;
88
using BudgetExperiment.Infrastructure.Persistence;
99

10+
using Microsoft.EntityFrameworkCore;
1011
using Microsoft.Extensions.DependencyInjection;
1112

1213
namespace BudgetExperiment.Performance.Tests.Infrastructure;
@@ -35,13 +36,24 @@ public static class TestDataSeeder
3536

3637
/// <summary>
3738
/// Seeds the database with realistic data for performance testing.
39+
/// When <see cref="PerformanceWebApplicationFactory.UseRealDb"/> is true,
40+
/// skips seeding but resolves <see cref="FirstAccountId"/> from the existing data.
3841
/// </summary>
3942
/// <param name="factory">The web application factory providing the service scope.</param>
4043
/// <returns>A task representing the asynchronous operation.</returns>
4144
public static async Task SeedAsync(PerformanceWebApplicationFactory factory)
4245
{
43-
using var scope = factory.Services.CreateScope();
44-
var db = scope.ServiceProvider.GetRequiredService<BudgetDbContext>();
46+
if (PerformanceWebApplicationFactory.UseRealDb)
47+
{
48+
using var realScope = factory.Services.CreateScope();
49+
var realDb = realScope.ServiceProvider.GetRequiredService<BudgetDbContext>();
50+
var account = await realDb.Accounts.FirstOrDefaultAsync();
51+
FirstAccountId = account?.Id ?? Guid.Empty;
52+
return;
53+
}
54+
55+
using var seedScope = factory.Services.CreateScope();
56+
var db = seedScope.ServiceProvider.GetRequiredService<BudgetDbContext>();
4557

4658
var categories = SeedCategories(db);
4759
var accounts = SeedAccounts(db);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) BecauseImClever. All rights reserved.
2+
3+
using NBomber.Contracts;
4+
using NBomber.CSharp;
5+
6+
namespace BudgetExperiment.Performance.Tests.Profiles;
7+
8+
/// <summary>
9+
/// Reduced stress profile for the calendar endpoint, which performs 9 sequential
10+
/// DB queries per request and degrades rapidly at high concurrency.
11+
/// Uses 25 req/s (vs 100 for the standard stress profile) to identify degradation
12+
/// without creating an unbounded request backlog.
13+
/// </summary>
14+
public static class CalendarStressProfile
15+
{
16+
/// <summary>
17+
/// Gets the calendar stress simulation: ramp to 25 req/s over 30 seconds,
18+
/// then sustain 25 req/s for 30 seconds.
19+
/// </summary>
20+
/// <returns>A load simulation array.</returns>
21+
public static LoadSimulation[] Simulations()
22+
{
23+
return
24+
[
25+
Simulation.RampingInject(
26+
rate: 25,
27+
interval: TimeSpan.FromSeconds(1),
28+
during: TimeSpan.FromSeconds(30)),
29+
Simulation.Inject(
30+
rate: 25,
31+
interval: TimeSpan.FromSeconds(1),
32+
during: TimeSpan.FromSeconds(30)),
33+
];
34+
}
35+
}

tests/BudgetExperiment.Performance.Tests/Profiles/StressProfile.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,31 @@
66
namespace BudgetExperiment.Performance.Tests.Profiles;
77

88
/// <summary>
9-
/// Stress test profile: ramps from 10 to 100 concurrent requests per second
10-
/// over 120 seconds to find the system&apos;s breaking point.
9+
/// Stress test profile: ramps to 30 concurrent requests per second
10+
/// over 30 seconds, then sustains for another 30 seconds.
11+
/// Scaled for in-memory WebApplicationFactory testing; 30 req/s is 3x
12+
/// the load test baseline, pushing beyond normal load while remaining
13+
/// completable on local hardware.
1114
/// </summary>
1215
public static class StressProfile
1316
{
1417
/// <summary>
15-
/// Gets the stress load simulation: ramp from 10 to 100 req/s over 60 seconds,
16-
/// then sustain 100 req/s for another 60 seconds.
18+
/// Gets the stress load simulation: ramp to 30 req/s over 30 seconds,
19+
/// then sustain 30 req/s for another 30 seconds.
1720
/// </summary>
1821
/// <returns>A load simulation array.</returns>
1922
public static LoadSimulation[] Simulations()
2023
{
2124
return
2225
[
2326
Simulation.RampingInject(
24-
rate: 100,
27+
rate: 30,
2528
interval: TimeSpan.FromSeconds(1),
26-
during: TimeSpan.FromSeconds(60)),
29+
during: TimeSpan.FromSeconds(30)),
2730
Simulation.Inject(
28-
rate: 100,
31+
rate: 30,
2932
interval: TimeSpan.FromSeconds(1),
30-
during: TimeSpan.FromSeconds(60)),
33+
during: TimeSpan.FromSeconds(30)),
3134
];
3235
}
3336
}

tests/BudgetExperiment.Performance.Tests/StressTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,14 @@ public void Transactions_StressTest()
110110

111111
/// <summary>
112112
/// Calendar endpoint under stress — the most complex read endpoint (9 sequential DB queries).
113+
/// Uses a reduced stress profile (25 req/s vs 100) because the calendar endpoint degrades
114+
/// rapidly at high concurrency, creating unbounded request backlogs at 100 req/s.
113115
/// Stress tests observe degradation without hard latency thresholds.
114116
/// </summary>
115117
[Fact]
116118
public void Calendar_StressTest()
117119
{
118-
var scenario = CalendarScenario.Create(this._client, StressProfile.Simulations())
120+
var scenario = CalendarScenario.Create(this._client, CalendarStressProfile.Simulations())
119121
.WithThresholds(
120122
Threshold.Create(stats => stats.Fail.Request.Percent < 5));
121123

0 commit comments

Comments
 (0)