HTML Test Report
TUnit automatically generates a rich HTML test report after every test run. No configuration is needed — the report is always on by default.
Report Location
After running your tests, the report is written to:
TestResults/{AssemblyName}-{os}-{tfm}-report.html
For example: TestResults/MyTests-linux-net10.0-report.html
The OS and runtime version are included automatically so that matrix builds (multiple platforms/TFMs) produce distinct files instead of overwriting each other.
Open it in any modern browser. The report is fully self-contained (single HTML file) and works offline.
Features
- Summary Dashboard — Total, passed, failed, skipped, and cancelled counts with a visual pass-rate donut chart
- Expandable Test Details — Click any test to see exception details, stack traces, stdout/stderr output, timing breakdown, categories, and custom properties
- Search — Type-ahead search to quickly filter tests by name, class, or category
- Status Filters — Filter by All, Passed, Failed, Skipped, or Cancelled
- Class Grouping — Tests are grouped by class with collapsible sections. Groups with failures auto-expand.
- OpenTelemetry Trace Timeline — When your tests use
System.Diagnostics.Activity, a waterfall timeline shows nested spans with durations - Retry Support — Tests with retries show the final result with a retry badge
Configuration
Custom Output Path
dotnet run -- --report-html-filename my-custom-report.html
Disable Report Generation
Set the environment variable:
export TUNIT_DISABLE_HTML_REPORTER=true
Accepts: true, 1, yes (case-insensitive).
Deprecated: --report-html Flag
The --report-html flag is deprecated since the report is now generated by default. Using it will show a deprecation warning but will not cause an error.
GitHub Actions Integration
When TUnit detects it is running inside GitHub Actions (GITHUB_ACTIONS=true), it will:
- Upload the report as a workflow artifact (if the required runtime token is available)
- Add a link to the GitHub step summary so you can find the report directly from the workflow run page
Enabling Automatic Artifact Upload
GitHub Actions does not expose ACTIONS_RUNTIME_TOKEN to shell run: steps by default — it is only available inside JavaScript and Docker action handlers. To make it available to your test process, add one of the following steps before your test step.
Option A: actions/github-script (recommended, first-party)
- name: Expose GitHub Actions Runtime
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']);
core.exportVariable('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']);
- name: Run Tests
run: dotnet run --project MyTests
Option B: crazy-max/ghaction-github-runtime
- name: Expose GitHub Actions Runtime
uses: crazy-max/ghaction-github-runtime@v3
- name: Run Tests
run: dotnet run --project MyTests
Option C: Manual upload-artifact step
If you prefer not to expose the runtime token, you can upload the report yourself:
- name: Run Tests
run: dotnet run --project MyTests
- name: Upload HTML Test Report
uses: actions/upload-artifact@v4
if: always()
with:
name: TestReport
path: '**/*-report.html'
Viewing the Report
After the workflow run completes:
- Go to the workflow run page
- Look for the artifact link in the step summary (Options A/B), or
- Find the report in the Artifacts section at the bottom of the page (all options)
Distributed Tracing
Distributed tracing requires .NET 8 or later.
The HTML report automatically captures trace spans from all instrumented .NET libraries — not just TUnit's own spans. When your test code calls HttpClient, ASP.NET Core, EF Core, or any library that emits System.Diagnostics.Activity spans, those spans appear as children in the test's trace timeline with no extra configuration.
How It Works
TUnit's test body runs under a System.Diagnostics.Activity. Because Activity.Current flows through async calls via AsyncLocal, any instrumented library automatically creates child spans under the same trace. The HTML report collects these and renders them in the test's timeline.
For example, an integration test using WebApplicationFactory:
[Test]
public async Task GetUsers_ReturnsOk()
{
var client = Factory.CreateClient();
var response = await client.GetAsync("/api/users"); // HttpClient + ASP.NET Core spans captured automatically
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
}
The trace timeline for this test will show the HttpClient request span, ASP.NET Core hosting span, and any middleware or database spans — all nested under the test's root span.
Linking External Traces
If your test communicates with an external service that runs in a separate process (and therefore has a different trace context), you can manually link its trace to the test:
[Test]
public async Task ProcessOrder_SendsNotification()
{
// Start some external work that creates its own trace
var externalActivity = MyExternalService.StartProcessing(orderId);
// Link that trace to this test so it appears in the HTML report
TestContext.Current!.RegisterTrace(externalActivity.Context.TraceId);
// ... wait for processing, assert results
}
Linked traces appear as a separate "Linked Trace" section below the test's main trace timeline, labeled by the source name of the spans.
Accessing the Test Activity
You can access the current test's Activity to parent external work explicitly:
[Test]
public async Task MyTest()
{
// Get the test's activity for manual context propagation
var testActivity = TestContext.Current!.Activity;
// Use its context to parent work under this test's trace
using var childActivity = new ActivitySource("MyApp")
.StartActivity("custom-work", ActivityKind.Internal, testActivity!.Context);
// ... do work — this span will appear in the test's trace timeline
}
This is useful when calling libraries that don't automatically propagate Activity.Current or when you need to create custom spans for visibility.
What Gets Captured
| Source | Captured Automatically? | Notes |
|---|---|---|
| TUnit spans (test lifecycle) | Yes | Always captured |
HttpClient (System.Net.Http) | Yes | When called from test context |
ASP.NET Core (Microsoft.AspNetCore) | Yes | Including via WebApplicationFactory |
EF Core (Microsoft.EntityFrameworkCore) | Yes | Database query spans |
| Other instrumented libraries | Yes | Any library using System.Diagnostics.Activity |
| External processes | No | Use TestContext.RegisterTrace() to link |
Overhead
The collector uses smart sampling: spans from known test traces are fully recorded, while unrelated activities receive only PropagationData (near-zero cost — no timing or tags collected). TUnit's own spans are always captured. External spans (HttpClient, ASP.NET Core, etc.) are capped at 100 per test to keep the report size manageable.
Troubleshooting
Report Not Generated
- Check that
TUNIT_DISABLE_HTML_REPORTERis not set in your environment - Verify that the
TestResults/directory is writable - Check the console output for any warning messages about report generation failures
Large Test Suites
For test suites with 1,000+ tests, the report uses client-side rendering from embedded JSON data. All search and filtering happens in the browser without page reloads.
Trace Timeline Not Showing
The trace timeline requires System.Diagnostics.Activity spans to be active during the test run. If your tests don't create activities, the trace section won't appear. TUnit's internal activities are captured automatically on .NET 8.0+.
External Library Spans Not Appearing
If spans from HttpClient, ASP.NET Core, or other libraries aren't showing in the trace timeline:
- Ensure .NET 8+ — distributed trace collection is not available on .NET Framework or .NET Standard targets
- Check the library is instrumented — the library must use
System.Diagnostics.Activity(most modern .NET libraries do) - Verify async context flow — the library call must happen within the test's async context (i.e., awaited from the test method or a hook). Fire-and-forget calls (
Task.Runwithout await) may lose theActivity.Currentcontext