A powerful load testing framework for .NET applications that seamlessly integrates with xUnit v3. Features actor-based architecture using Akka.NET, fluent API for test configuration, comprehensive performance metrics, and production-ready error handling. Perfect for testing APIs, databases, and web applications under load.
$ dotnet add package xUnitV3LoadFrameworkxUnit v3-native load-style test runner for executing actions concurrently over a duration and reporting throughput/success/failure.
Best for:
dotnet testNot for:
dotnet add package xUnitV3LoadFramework
This framework is a true xUnit v3 extension:
dotnet test — no extra tooling needed--filterThe test method body runs automatically under load — no manual ExecuteAsync() call needed:
using xUnitV3LoadFramework.Attributes;
public class ApiLoadTests
{
private static readonly HttpClient _httpClient = new();
[Load(concurrency: 5, duration: 3000, interval: 500)]
public async Task Api_Should_Handle_Concurrent_Requests()
{
// This entire method body runs N times under load
var response = await _httpClient.GetAsync("https://api.example.com/health");
response.EnsureSuccessStatusCode();
}
}
Test passes if all iterations complete without exception. Test fails if any iteration throws or returns false.
Supported return types:
async Task — success if no exceptionvoid — success if no exceptionTask<bool> / ValueTask<bool> — success if returns truebool — success if returns trueusing xUnitV3LoadFramework.Extensions;
public class ApiLoadTests
{
private static readonly HttpClient _httpClient = new();
[Fact]
public async Task Api_Load_Test_Fluent()
{
var result = await LoadTestRunner.Create()
.WithName("HealthCheck_Load")
.WithConcurrency(10)
.WithDuration(TimeSpan.FromSeconds(5))
.WithInterval(TimeSpan.FromMilliseconds(200))
.RunAsync(async () =>
{
var response = await _httpClient.GetAsync("https://api.example.com/health");
response.EnsureSuccessStatusCode();
});
Assert.True(result.Success >= result.Total * 0.95);
}
}
[Load], [Fact], and [Theory] can coexist in the same test class:
public class ApiTests
{
private static readonly HttpClient _httpClient = new();
[Fact]
public void Should_Have_Valid_BaseUrl()
{
Assert.NotNull(_httpClient.BaseAddress);
}
[Theory]
[InlineData("/health")]
[InlineData("/ready")]
public async Task Endpoint_Should_Exist(string path)
{
var response = await _httpClient.GetAsync(path);
Assert.True(response.IsSuccessStatusCode);
}
[Load(concurrency: 5, duration: 3000, interval: 500)]
public async Task Api_Should_Handle_Load()
{
var response = await _httpClient.GetAsync("/health");
response.EnsureSuccessStatusCode();
}
}
Native [Load] test output:
Load Test Results:
Total: 30, Success: 28, Failure: 2
RPS: 9.8, Avg: 102ms, P95: 150ms, P99: 180ms
Result: FAILED (93.3% success rate)
Fluent API test output:
Load test 'HealthCheck_Load' completed:
Total executions: 50
Successful executions: 48
Failed executions: 2
Execution time: 5.12 seconds
Requests per second: 9.77
Average latency: 102.34ms
Success rate: 96.00%
| Setting | Description |
|---|---|
| Concurrency | Number of concurrent operations launched per interval |
| Duration | Total time the load test runs |
| Interval | Time between launching batches of concurrent operations |
| TerminationMode | How the test stops: Duration (immediate), CompleteCurrentInterval (finish current batch), or StrictDuration (exact timing) |
| GracefulStopTimeout | Max time to wait for in-flight requests after duration expires. Default: 30% of duration, bounded 5-60s |
| Success | Action returns true or completes without exception |
| Failure | Action returns false or throws an exception |
| RequestsPerSecond | Total / Time — completed operations per second |
Every Interval, the framework launches Concurrency concurrent operations. For example:
Concurrency: 5, Duration: 3s, Interval: 500ms = 6 batches × 5 operations = ~30 total operationsFor full control, use LoadExecutionPlan with LoadRunner.Run():
using LoadSurge.Models;
using LoadSurge.Runner;
[Fact]
public async Task Advanced_Load_Test()
{
var plan = new LoadExecutionPlan
{
Name = "Database_Connection_Pool",
Settings = new LoadSettings
{
Concurrency = 20,
Duration = TimeSpan.FromSeconds(30),
Interval = TimeSpan.FromMilliseconds(100),
TerminationMode = TerminationMode.CompleteCurrentInterval,
GracefulStopTimeout = TimeSpan.FromSeconds(10)
},
Action = async () =>
{
using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
return true;
}
};
var result = await LoadRunner.Run(plan);
Assert.True(result.Success >= result.Total * 0.99);
}
| Mode | Behavior |
|---|---|
Duration | Stops immediately when duration expires (default) |
CompleteCurrentInterval | Waits for current batch to finish before stopping |
StrictDuration | Strict timing — may cut off final batch |
Stop after a fixed number of operations regardless of duration:
var result = await LoadTestRunner.Create()
.WithConcurrency(10)
.WithDuration(TimeSpan.FromMinutes(5))
.WithMaxIterations(1000) // Stop after 1000 operations
.RunAsync(async () => { /* ... */ });
Use LoadResult fields to fail tests based on performance criteria:
var result = await LoadTestRunner.Create()
.WithConcurrency(10)
.WithDuration(TimeSpan.FromSeconds(10))
.RunAsync(async () => { /* ... */ });
// Success rate gate
var successRate = (double)result.Success / result.Total;
Assert.True(successRate >= 0.99, $"Success rate {successRate:P} below 99%");
// Throughput gate
Assert.True(result.RequestsPerSecond >= 50, $"RPS {result.RequestsPerSecond} below 50");
// Latency gate
Assert.True(result.Percentile95Latency < 500, $"P95 latency {result.Percentile95Latency}ms exceeds 500ms");
Assert.True(result.AverageLatency < 200, $"Avg latency {result.AverageLatency}ms exceeds 200ms");
Total, Success, Failure — countsTime — execution time in secondsRequestsPerSecond — throughputAverageLatency, MinLatency, MaxLatency — in millisecondsMedianLatency, Percentile95Latency, Percentile99Latency — percentiles in msPeakMemoryUsage — bytesThe framework runs all iterations to completion before determining pass/fail:
Failure > 0This means you always get complete metrics, even when some iterations fail.
Native [Load] tests: Pass/fail is automatic based on iteration results.
Fluent API tests: You control pass/fail with assertions:
var result = await LoadTestRunner.Create()
.WithConcurrency(10)
.WithDuration(TimeSpan.FromSeconds(5))
.RunAsync(async () => { /* ... */ });
// Allow up to 5% failure rate
var successRate = (double)result.Success / result.Total;
Assert.True(successRate >= 0.95, $"Success rate {successRate:P} below 95%");
static readonly HttpClient — don't instantiate per request.using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await httpClient.GetAsync(url, cts.Token);
GracefulStopTimeout to wait for in-flight requests. Long-running actions without cancellation support may delay test completion.dotnet test --filter "FullyQualifiedName~LoadTests" to run only load tests or exclude them from fast CI runs.Use this framework when:
dotnet test without extra toolingUse a dedicated tool when:
Think of this like a playground stress test. You set:
Concurrency)Duration)Interval)The framework tells you how many kids had fun (success), how many fell off the swings (failure), and how fast the line moved (RPS).
PRs welcome. Open an issue for bugs or feature requests.
Made by Vasyl