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.

A machine-readable JSON sidecar ({AssemblyName}-{os}-{tfm}.tunit-report.json) is written alongside the HTML report. It powers report aggregation — merging reports from multiple test projects into one — and can be disabled with TUNIT_DISABLE_JSON_REPORT=true or context.Settings.Reporting.JsonReportEnabled = false.

Running many test projects and want one combined report instead of one per project? See Aggregated Reports.

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​

When TUnit is the only HTML report provider, the existing option remains available:

dotnet run -- --report-html-filename my-custom-report.html

When the Microsoft.Testing.Extensions.HtmlReport package is referenced, Microsoft owns the --report-html and --report-html-filename options. TUnit automatically switches to its namespaced filename option:

dotnet run -- --tunit-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).

For version-controlled project configuration, set context.Settings.Reporting.HtmlReportEnabled = false in a [Before(HookType.TestDiscovery)] hook instead.

Deprecated: --report-html Flag​

Without Microsoft.Testing.Extensions.HtmlReport, TUnit continues to accept the deprecated --report-html flag for compatibility. TUnit's report is generated automatically, so the flag has no effect beyond displaying a deprecation warning. Use TUNIT_DISABLE_HTML_REPORTER=true when you need to disable TUnit's report generation.

When Microsoft.Testing.Extensions.HtmlReport is referenced, TUnit does not register either legacy option; their behavior is provided by the Microsoft extension.

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: Enable Automated Test Report Artifact Upload
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: 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'

Setting Artifact Retention​

By default the uploaded artifact uses your repository's default retention period. To keep the report for a shorter time (and stay under your Actions storage quota), set the TUNIT_ARTIFACT_RETENTION_DAYS environment variable to the number of days you want:

- name: Run Tests
run: dotnet run --project MyTests
env:
TUNIT_ARTIFACT_RETENTION_DAYS: 5

The value must be a positive integer. If it exceeds your repository/organization maximum, TUnit clamps it to that maximum. This applies only to the automatic upload (Option A); the manual upload-artifact step (Option B) has its own retention-days input.

Disabling the Automatic Artifact Upload​

To keep generating the html report file but skip the automatic artifact upload, set the TUNIT_DISABLE_ARTIFACT_UPLOAD environment variable:

- name: Run Tests
run: dotnet run --project MyTests
env:
TUNIT_DISABLE_ARTIFACT_UPLOAD: true

This is useful if you:

  • Run a GitHub-actions compatible CI system that cant handle upload-artifact/v7 such as a GHES
  • Upload the report to another endpoint anyway

The report file and the GITHUB_STEP_SUMMARY are still generated.

This can also be configured in code with context.Settings.Reporting.ArtifactUploadEnabled = false.

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 (Option A), 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. TUnit collects both per-test traces ("TUnit") and runner lifecycle traces ("TUnit.Lifecycle"). 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 per-test 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 renders that per-test trace in each test's detail view, and separately uses TUnit's lifecycle spans for the class and execution timelines.

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.

Same data, different views

The HTML report groups each test under its class. Backends like Seq, Jaeger, and the Aspire dashboard show every test as its own trace — that's by design. See Distributed Tracing for why, and how to group spans across tests in your backend.

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()
{
// Obtain this from the external service's trace response or diagnostics.
var externalTraceId = ActivityTraceId.CreateRandom();

// Link that trace to this test so it appears in the HTML report
TestContext.Current!.RegisterTrace(externalTraceId);

// ... 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)YesPer-test traces plus session/assembly/suite timelines
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 and context.Settings.Reporting.HtmlReportEnabled is not false
  • 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