Skip to main content

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:

  1. Upload the report as a workflow artifact (if the required runtime token is available)
  2. 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.

- 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:

  1. Go to the workflow run page
  2. Look for the artifact link in the step summary (Options A/B), or
  3. Find the report in the Artifacts section at the bottom of the page (all options)

Distributed Tracing

note

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

SourceCaptured Automatically?Notes
TUnit spans (test lifecycle)YesAlways captured
HttpClient (System.Net.Http)YesWhen called from test context
ASP.NET Core (Microsoft.AspNetCore)YesIncluding via WebApplicationFactory
EF Core (Microsoft.EntityFrameworkCore)YesDatabase query spans
Other instrumented librariesYesAny library using System.Diagnostics.Activity
External processesNoUse 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_REPORTER is 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.Run without await) may lose the Activity.Current context