-
-
Notifications
You must be signed in to change notification settings - Fork 114
Description
Problem
Setting up Aspire distributed application testing with TUnit requires significant boilerplate. Every Aspire test project needs to:
- Create a fixture that starts/stops the
DistributedApplication - Wait for all resources to reach
Runningstate with proper timeouts - Handle disposal and cleanup
- Provide
CreateHttpClient()andGetConnectionStringAsync()helpers - Wire up nested fixtures (database, Redis, RabbitMQ connections) that depend on the app fixture
This is ~50+ lines of boilerplate that every Aspire test project must replicate, and it's easy to get wrong (missing disposal, incorrect resource waiting, timeout handling, etc.).
// Current: manual boilerplate in every project
public class DistributedAppFixture : IAsyncInitializer, IAsyncDisposable
{
private DistributedApplication? _app;
public async Task InitializeAsync()
{
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.CloudShop_AppHost>();
builder.Services.ConfigureHttpClientDefaults(http =>
http.AddStandardResilienceHandler());
_app = await builder.BuildAsync();
await _app.StartAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
await _app.ResourceNotifications
.WaitForResourceAsync("postgres", KnownResourceStates.Running, cts.Token);
await _app.ResourceNotifications
.WaitForResourceAsync("redis", KnownResourceStates.Running, cts.Token);
// ... repeat for every resource
}
public HttpClient CreateHttpClient(string name) => _app!.CreateHttpClient(name);
public async ValueTask DisposeAsync()
{
if (_app is not null) { await _app.StopAsync(); await _app.DisposeAsync(); }
}
}Proposed Package: TUnit.Aspire
Core: AspireFixture<TAppHost>
A base class (similar to how TUnit.AspNetCore provides WebApplicationTest<TFactory, TEntryPoint>) that handles the Aspire app lifecycle:
public class AppFixture : AspireFixture<Projects.CloudShop_AppHost>
{
// Override to customize the builder (optional)
protected override void ConfigureBuilder(IDistributedApplicationTestingBuilder builder)
{
builder.Services.ConfigureHttpClientDefaults(http =>
http.AddStandardResilienceHandler());
}
// Override to specify resource wait timeout (optional, default 60s)
protected override TimeSpan ResourceTimeout => TimeSpan.FromSeconds(120);
// Override to customize which resources to wait for (optional, default: all)
protected override ResourceWaitStrategy WaitStrategy => ResourceWaitStrategy.All;
}AspireFixture<TAppHost> should provide:
- Automatic lifecycle:
IAsyncInitializer+IAsyncDisposable— build, start, stop, dispose - Resource waiting: Automatically wait for all (or specified) resources to reach
Runningstate before tests begin - Configurable timeout:
ResourceTimeoutproperty with sensible default (60s) CreateHttpClient(string resourceName): Create pre-configured HTTP clients for named resourcesGetConnectionStringAsync(string resourceName): Get connection strings for infrastructure resourcesGetResourceEndpoint(string resourceName): Get endpoint URIs for resources- Builder customization:
ConfigureBuilder()virtual method for adding services, configuration, etc. - Designed for
[ClassDataSource<T>(Shared = SharedType.PerTestSession)]: One app instance per test session
Resource Wait Strategies
public enum ResourceWaitStrategy
{
All, // Wait for all resources (default)
None, // Don't wait (manual control)
Named // Wait for specific resources only
}
// For Named strategy:
protected override IEnumerable<string> ResourcesToWaitFor()
{
yield return "postgres";
yield return "apiservice";
}Test Isolation Helpers
Following TUnit.AspNetCore's UniqueId / GetIsolatedName() pattern:
// Inherited from AspireFixture<T>
public int UniqueId { get; }
protected string GetIsolatedName(string baseName) => $"Test_{UniqueId}_{baseName}";Useful for creating isolated database names, Redis key prefixes, queue names, etc. in parallel test scenarios.
TUnit Logging Integration
// Aspire logs are automatically captured and forwarded to TUnit's test output
protected override LogLevel MinimumLogLevel => LogLevel.Information;Similar to TUnit.AspNetCore's AddTUnitLogging(), but wired into Aspire's DistributedApplicationTestingBuilder.
Nested Fixture Support
Works naturally with TUnit's nested [ClassDataSource] pattern for infrastructure fixtures:
// Database fixture that depends on the Aspire app fixture
public class DatabaseFixture : IAsyncInitializer, IAsyncDisposable
{
[ClassDataSource<AppFixture>(Shared = SharedType.PerTestSession)]
public required AppFixture App { get; init; }
public async Task InitializeAsync()
{
var connectionString = await App.GetConnectionStringAsync("postgres");
// Set up NpgsqlDataSource, etc.
}
}Complete Example
// Fixture - minimal code thanks to TUnit.Aspire
public class AppFixture : AspireFixture<Projects.CloudShop_AppHost>
{
protected override void ConfigureBuilder(IDistributedApplicationTestingBuilder builder)
{
builder.Services.ConfigureHttpClientDefaults(http =>
http.AddStandardResilienceHandler());
}
}
// Test class - clean and focused
public class ProductTests
{
[ClassDataSource<AppFixture>(Shared = SharedType.PerTestSession)]
public required AppFixture App { get; init; }
[Test]
public async Task Can_Get_Products()
{
var client = App.CreateHttpClient("apiservice");
var response = await client.GetAsync("/api/products");
await Assert.That(response.IsSuccessStatusCode).IsTrue();
}
}Scope
In Scope
AspireFixture<TAppHost>base class with lifecycle management- Resource waiting with configurable strategies and timeouts
CreateHttpClient/GetConnectionStringAsync/GetResourceEndpointhelpers- Builder customization via virtual methods
- Test isolation helpers (
UniqueId,GetIsolatedName) - TUnit logging integration
- Works with
[ClassDataSource]andSharedTypesharing patterns
Out of Scope (Future)
- HTTP exchange capture middleware (could be added later, similar to
TUnit.AspNetCore) - Aspire dashboard integration
- Custom health check polling beyond resource state
Context
Built a full Aspire + TUnit example in #4761. The DistributedAppFixture was ~50 lines of boilerplate that every Aspire test project would need to replicate. Aspire is a growing part of the .NET ecosystem and first-class TUnit support would be a strong differentiator. The design follows the same patterns established by TUnit.AspNetCore (WebApplicationTest<TFactory, TEntryPoint>).