diff --git a/TUnit.Aspire.Tests/ResourcesToRemoveTests.cs b/TUnit.Aspire.Tests/ResourcesToRemoveTests.cs
new file mode 100644
index 0000000000..83b3d043f8
--- /dev/null
+++ b/TUnit.Aspire.Tests/ResourcesToRemoveTests.cs
@@ -0,0 +1,46 @@
+using Microsoft.Extensions.DependencyInjection;
+using TUnit.Assertions;
+using TUnit.Core;
+using Aspire.Hosting.ApplicationModel;
+
+namespace TUnit.Aspire.Tests;
+
+///
+/// Fixture that removes the nginx-no-healthcheck resource before the app starts.
+///
+public class FixtureWithRemovedResource : AspireFixture
+{
+ protected override TimeSpan ResourceTimeout => TimeSpan.FromSeconds(120);
+ protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.None;
+ protected override bool EnableTelemetryCollection => false;
+
+ protected override IEnumerable ResourcesToRemove() => ["nginx-no-healthcheck"];
+}
+
+///
+/// Verifies that resources listed in
+/// are removed from the distributed application before it starts.
+///
+[ClassDataSource(Shared = SharedType.PerTestSession)]
+[Category("Docker")]
+[Category("Integration")]
+public class ResourcesToRemoveTests(FixtureWithRemovedResource fixture)
+{
+ [Test]
+ public async Task RemovedResource_IsNotPresentInAppModel()
+ {
+ var model = fixture.App.Services.GetRequiredService();
+ var resourceNames = model.Resources.Select(r => r.Name).ToList();
+
+ await Assert.That(resourceNames).DoesNotContain("nginx-no-healthcheck");
+ }
+
+ [Test]
+ public async Task OtherResources_AreStillPresent()
+ {
+ var model = fixture.App.Services.GetRequiredService();
+ var resourceNames = model.Resources.Select(r => r.Name).ToList();
+
+ await Assert.That(resourceNames).Contains("api-service");
+ }
+}
diff --git a/TUnit.Aspire/AspireFixture.cs b/TUnit.Aspire/AspireFixture.cs
index 50ee4d70b3..8acabf65e4 100644
--- a/TUnit.Aspire/AspireFixture.cs
+++ b/TUnit.Aspire/AspireFixture.cs
@@ -191,6 +191,11 @@ protected virtual void ConfigureBuilder(IDistributedApplicationTestingBuilder bu
///
protected virtual IEnumerable ResourcesToWaitFor() => [];
+ ///
+ /// Resources to be removed from the .
+ ///
+ protected virtual IEnumerable ResourcesToRemove() => [];
+
///
/// Override for full control over the resource waiting logic.
///
@@ -258,6 +263,7 @@ public virtual async Task InitializeAsync()
LogProgress($"Creating distributed application builder for {typeof(TAppHost).Name}...");
var builder = await DistributedApplicationTestingBuilder.CreateAsync(Args, ConfigureAppHost);
ConfigureBuilder(builder);
+ RemoveResources(builder);
// Configure OTLP endpoint on project resources AFTER user's ConfigureBuilder
if (_otlpReceiver is not null)
@@ -324,6 +330,25 @@ public virtual async Task InitializeAsync()
}
}
+ private void RemoveResources(IDistributedApplicationTestingBuilder builder)
+ {
+ foreach (var name in ResourcesToRemove())
+ {
+ var resource =
+ builder.Resources.SingleOrDefault(r =>
+ string.Equals(r.Name, name, StringComparison.Ordinal));
+
+ if (resource is not null)
+ {
+ builder.Resources.Remove(resource);
+ }
+ else
+ {
+ LogProgress($"ResourcesToRemove: resource '{name}' not found (skipped).");
+ }
+ }
+ }
+
///
/// Disposes the Aspire distributed application. Override to add custom cleanup:
///
diff --git a/docs/docs/examples/aspire.md b/docs/docs/examples/aspire.md
index b23b1bec50..27c366afb1 100644
--- a/docs/docs/examples/aspire.md
+++ b/docs/docs/examples/aspire.md
@@ -9,10 +9,11 @@ dotnet add package TUnit.Aspire
```
:::info Prerequisites
+
- An Aspire AppHost project in your solution
- Docker running (Aspire uses containers for infrastructure resources)
- .NET 8.0 or later
-:::
+ :::
## Quick Start
@@ -37,6 +38,7 @@ public class ApiTests(AspireFixture fixture)
```
That's it. The fixture will:
+
1. Build your Aspire AppHost
2. Start all containers and projects
3. Wait for all resources to become healthy
@@ -151,16 +153,30 @@ public class AppFixture : AspireFixture
Available `ResourceWaitBehavior` values:
-| Value | Description |
-|-------|-------------|
-| `AllHealthy` | Wait for all resources to pass health checks (default) |
-| `AllRunning` | Wait for all resources to reach the Running state |
-| `Named` | Wait only for resources returned by `ResourcesToWaitFor()` |
-| `None` | Don't wait — handle readiness manually in tests |
+| Value | Description |
+| ------------ | ---------------------------------------------------------- |
+| `AllHealthy` | Wait for all resources to pass health checks (default) |
+| `AllRunning` | Wait for all resources to reach the Running state |
+| `Named` | Wait only for resources returned by `ResourcesToWaitFor()` |
+| `None` | Don't wait — handle readiness manually in tests |
-### Timeouts
+### Removing Resources
+
+Use `ResourcesToRemove()` to exclude specific resources from the distributed application before it is built. This is useful when your AppHost defines UI tools or optional infrastructure (e.g. `pgAdmin`, `RedisInsight`, `seq`) that are not needed — and potentially slow to start — during automated tests:
+
+```csharp
+public class AppFixture : AspireFixture
+{
+ protected override IEnumerable ResourcesToRemove()
+ => ["pgadmin", "redisinsight", "seq"];
+}
+```
-The `ResourceTimeout` controls how long the fixture waits for both `StartAsync()` and resource readiness:
+:::tip
+Resources are removed by exact name (case-sensitive) after the builder is created but before the app is built, so they never start. Unrecognised names are silently ignored.
+:::
+
+### Timeouts
```csharp
public class AppFixture : AspireFixture
@@ -171,6 +187,7 @@ public class AppFixture : AspireFixture
```
When a timeout occurs, the error includes:
+
- Which resources are ready vs. still pending
- Recent container logs from pending resources
- Diagnostic information about the failure
@@ -179,33 +196,34 @@ When a timeout occurs, the error includes:
### Properties
-| Property | Type | Description |
-|----------|------|-------------|
-| `App` | `DistributedApplication` | The running Aspire app. Access for advanced scenarios. |
+| Property | Type | Description |
+| -------- | ------------------------ | ------------------------------------------------------ |
+| `App` | `DistributedApplication` | The running Aspire app. Access for advanced scenarios. |
### Methods
-| Method | Returns | Description |
-|--------|---------|-------------|
-| `CreateHttpClient(resourceName, endpointName?)` | `HttpClient` | Creates an HTTP client connected to the named resource. When telemetry collection is enabled, automatically propagates `traceparent` and `baggage` headers for cross-process correlation. |
-| `GetConnectionStringAsync(resourceName, ct?)` | `Task` | Gets the connection string for the named resource |
-| `WatchResourceLogs(resourceName)` | `IAsyncDisposable` | Streams resource logs to the current test's output |
+| Method | Returns | Description |
+| ----------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `CreateHttpClient(resourceName, endpointName?)` | `HttpClient` | Creates an HTTP client connected to the named resource. When telemetry collection is enabled, automatically propagates `traceparent` and `baggage` headers for cross-process correlation. |
+| `GetConnectionStringAsync(resourceName, ct?)` | `Task` | Gets the connection string for the named resource |
+| `WatchResourceLogs(resourceName)` | `IAsyncDisposable` | Streams resource logs to the current test's output |
### Virtual Methods (Override to Customize)
-| Method | Default | Description |
-|--------|---------|-------------|
-| `InitializeAsync()` | Full lifecycle | Override to add post-start logic (migrations, seeding) |
-| `DisposeAsync()` | Stop and dispose app | Override to add custom cleanup |
-| `Args` | Empty | Command-line arguments passed to the AppHost entry point |
-| `ConfigureAppHost(options, settings)` | No-op | Configure `DistributedApplicationOptions` and `HostApplicationBuilderSettings` during builder creation |
-| `ConfigureBuilder(builder)` | No-op | Customize the builder before building |
-| `EnableTelemetryCollection` | `true` | Starts an OTLP receiver that correlates SUT logs to the originating test |
-| `ResourceTimeout` | 60 seconds | How long to wait for startup and resources |
-| `WaitBehavior` | `AllHealthy` | Which resources to wait for |
-| `ResourcesToWaitFor()` | Empty | Resource names when `WaitBehavior` is `Named` |
-| `WaitForResourcesAsync(app, ct)` | Waits per `WaitBehavior` | Full control over resource waiting |
-| `LogProgress(message)` | Writes to stderr | Override to route progress logs elsewhere |
+| Method | Default | Description |
+| ------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
+| `InitializeAsync()` | Full lifecycle | Override to add post-start logic (migrations, seeding) |
+| `DisposeAsync()` | Stop and dispose app | Override to add custom cleanup |
+| `Args` | Empty | Command-line arguments passed to the AppHost entry point |
+| `ConfigureAppHost(options, settings)` | No-op | Configure `DistributedApplicationOptions` and `HostApplicationBuilderSettings` during builder creation |
+| `ConfigureBuilder(builder)` | No-op | Customize the builder before building |
+| `EnableTelemetryCollection` | `true` | Starts an OTLP receiver that correlates SUT logs to the originating test |
+| `ResourceTimeout` | 60 seconds | How long to wait for startup and resources |
+| `WaitBehavior` | `AllHealthy` | Which resources to wait for |
+| `ResourcesToWaitFor()` | Empty | Resource names when `WaitBehavior` is `Named` |
+| `ResourcesToRemove()` | Empty | Resource names to remove from the builder before the app is built |
+| `WaitForResourcesAsync(app, ct)` | Waits per `WaitBehavior` | Full control over resource waiting |
+| `LogProgress(message)` | Writes to stderr | Override to route progress logs elsewhere |
### Overriding the Lifecycle
@@ -250,10 +268,11 @@ public class AppFixture : AspireFixture
```
:::tip When to use `Args` vs `ConfigureAppHost` vs `ConfigureBuilder`
-- Use **`Args`** for configuration values that the AppHost reads during `CreateBuilder(args)` — these must be set *before* the builder is created.
+
+- Use **`Args`** for configuration values that the AppHost reads during `CreateBuilder(args)` — these must be set _before_ the builder is created.
- Use **`ConfigureAppHost`** to configure `DistributedApplicationOptions` (e.g., `DisableDashboard`) and `HostApplicationBuilderSettings` — these are passed to `CreateAsync` during builder creation.
-- Use **`ConfigureBuilder`** for service registrations, HTTP client defaults, and other configuration that can be applied *after* the builder is created.
-:::
+- Use **`ConfigureBuilder`** for service registrations, HTTP client defaults, and other configuration that can be applied _after_ the builder is created.
+ :::
## Watching Resource Logs
@@ -308,13 +327,13 @@ When multiple tests run concurrently against the same resource, each test only s
The SUT must have OpenTelemetry configured to export logs and traces via OTLP. `AspireFixture` automatically injects the following environment variables into all project resources:
-| Variable | Value | Purpose |
-|----------|-------|---------|
-| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://127.0.0.1:{port}` | Points to TUnit's OTLP receiver |
-| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | Protocol for OTLP export |
-| `OTEL_SERVICE_NAME` | Aspire resource name | Shown as `[service-name]` prefix in test output |
-| `OTEL_BLRP_SCHEDULE_DELAY` | `1000` | Reduces log batch export delay for faster test feedback |
-| `OTEL_BSP_SCHEDULE_DELAY` | `1000` | Reduces span batch export delay for faster test feedback |
+| Variable | Value | Purpose |
+| ----------------------------- | ------------------------- | -------------------------------------------------------- |
+| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://127.0.0.1:{port}` | Points to TUnit's OTLP receiver |
+| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | Protocol for OTLP export |
+| `OTEL_SERVICE_NAME` | Aspire resource name | Shown as `[service-name]` prefix in test output |
+| `OTEL_BLRP_SCHEDULE_DELAY` | `1000` | Reduces log batch export delay for faster test feedback |
+| `OTEL_BSP_SCHEDULE_DELAY` | `1000` | Reduces span batch export delay for faster test feedback |
The SUT only needs to register the OpenTelemetry exporters — TUnit handles everything else.
@@ -347,6 +366,7 @@ builder.Logging.AddOpenTelemetry(otel =>
Note that `OTEL_SERVICE_NAME` and `OTEL_EXPORTER_OTLP_ENDPOINT` are injected by TUnit, so the SUT does not need `.ConfigureResource()` or any endpoint configuration.
The key pieces are:
+
- **`AddAspNetCoreInstrumentation()`** — ensures incoming HTTP requests create spans that carry the test's TraceId, so logs within that request context inherit it.
- **`AddOtlpExporter()`** on both tracing and logging — exports telemetry to TUnit's OTLP receiver (endpoint is injected automatically).
- **`IncludeFormattedMessage = true`** — without this, log bodies are empty in the test output. Aspire `ServiceDefaults` sets this by default.
@@ -576,6 +596,7 @@ dotnet new tunit-aspire-test -n MyApp.Tests
**Symptom:** Tests time out during startup with no obvious error.
**Common causes:**
+
1. **TLS/SSL errors** — Set `ASPIRE_ALLOW_UNSECURED_TRANSPORT=true` or call `.WithoutHttpsCertificate()` on container resources in your AppHost.
2. **Docker images not pulled** — First run pulls container images, which can take minutes. Increase `ResourceTimeout`.
3. **Docker not running** — Aspire requires Docker. Verify with `docker info`.
@@ -611,7 +632,7 @@ public class AppBTests(AppBFixture fixture) { /* ... */ }
### How do I skip waiting for tool containers?
-Tool containers like pgAdmin or RedisInsight don't need to be ready before tests run. Use `Named` wait behavior:
+Tool containers like pgAdmin or RedisInsight don't need to be ready before tests run. If you want them to still run (e.g. for manual inspection), use `Named` wait behavior:
```csharp
public class AppFixture : AspireFixture
@@ -624,9 +645,20 @@ public class AppFixture : AspireFixture
}
```
+If you don't need them to run at all during tests, remove them entirely instead:
+
+```csharp
+public class AppFixture : AspireFixture
+{
+ protected override IEnumerable ResourcesToRemove()
+ => ["pgadmin", "redisinsight"];
+}
+```
+
### My resource never becomes healthy
If a resource stays in `Running` but never reaches `Healthy`, check:
+
1. The resource has a health check configured (`.WithHttpHealthCheck("/health")` or similar)
2. The health check endpoint is reachable from inside the container network
3. Use `WatchResourceLogs("resourceName")` in a test to see the resource's output
@@ -639,11 +671,11 @@ protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.All
### What's the difference between TUnit.Aspire and TUnit.AspNetCore?
-| | TUnit.Aspire | TUnit.AspNetCore |
-|---|---|---|
-| **Purpose** | Test distributed apps (multiple services + infrastructure) | Test a single ASP.NET Core app |
-| **Infrastructure** | Real containers via Aspire/Docker | In-process `TestServer` or Testcontainers |
-| **Isolation** | Shared app, per-test HTTP clients | Per-test `WebApplicationFactory` |
-| **Use when** | Your app uses Aspire orchestration | Your app is a single ASP.NET Core project |
+| | TUnit.Aspire | TUnit.AspNetCore |
+| ------------------ | ---------------------------------------------------------- | ----------------------------------------- |
+| **Purpose** | Test distributed apps (multiple services + infrastructure) | Test a single ASP.NET Core app |
+| **Infrastructure** | Real containers via Aspire/Docker | In-process `TestServer` or Testcontainers |
+| **Isolation** | Shared app, per-test HTTP clients | Per-test `WebApplicationFactory` |
+| **Use when** | Your app uses Aspire orchestration | Your app is a single ASP.NET Core project |
They can be used together — for example, using Aspire to manage infrastructure while using `TestWebApplicationFactory` for per-test app isolation.