Skip to content

Assertion Tracking

Aryeh Citron edited this page May 6, 2026 · 24 revisions

Added in v2.28.30

Assertion tracking renders your test assertions as styled inline notes in sequence diagrams. Passing assertions appear as green notes (✓), and failing assertions appear as red notes (✗) with the failure message.

This makes it immediately visible what was verified at each point in a test flow — directly inside the diagram, without navigating to test code.


Automatic Assertion Rewriting (Recommended)

Added in v2.30.0

The recommended approach is the TestTrackingDiagrams.AssertionRewriter package — an MSBuild task that automatically wraps your .Should() assertions at compile time. No changes to your test code required.

Requirements: The AssertionRewriter only works with FluentAssertions or AwesomeAssertions (i.e. assertions using the .Should() API). If you use a different assertion library, see Manual Tracking below.

SDK requirement: The AssertionRewriter MSBuild task requires the .NET 9 SDK or later (it depends on Roslyn 4.12.0 which ships with .NET 9). Your projects can still target net8.0 or earlier — only the build SDK needs to be 9.0+. If your global.json pins to an 8.x SDK, update it to "version": "9.0.0" with "rollForward": "latestFeature".

Compatibility: The AssertionRewriter operates as an MSBuild task that rewrites source files before compilation (BeforeTargets="CoreCompile"). It does not participate in the Roslyn source generator pipeline, so it coexists cleanly with all source generators (ReqNRoll, Blazor Razor, RegexGenerator, etc.) and other MSBuild-based code generation tools.

Quick Start

  1. Install the package:
<PackageReference Include="TestTrackingDiagrams.AssertionRewriter" Version="2.30.0" />
  1. Add the assembly-level opt-in attribute anywhere in your test project (commonly in a GlobalUsings.cs or AssertionTracking.cs file):
using TestTrackingDiagrams.Tracking;

[assembly: TrackAssertions]

The TrackAssertionsAttribute and SuppressAssertionTrackingAttribute types are auto-generated into your project at build time — no manual type definitions needed.

That's it. Your existing assertions now appear in diagrams automatically:

[Fact]
public async Task Creates_order_and_returns_201()
{
    var response = await _client.PostAsJsonAsync("/orders", new { Item = "Widget", Qty = 3 });

    response.StatusCode.Should().Be(HttpStatusCode.Created);  // ✓ appears in diagram

    var order = await response.Content.ReadFromJsonAsync<Order>();

    order!.Item.Should().Be("Widget");  // ✓ appears in diagram
    order!.Qty.Should().Be(3);          // ✓ appears in diagram
}

How It Works

The MSBuild task runs before compilation (BeforeTargets="CoreCompile") and:

  1. Auto-generates the TrackAssertionsAttribute and SuppressAssertionTrackingAttribute types into your project's intermediate output
  2. Scans for the [assembly: TrackAssertions] attribute in your source files
  3. Finds all expression statements containing .Should() calls
  4. Wraps them in Track.That(() => ...) (or Track.ThatAsync(async () => ...) for awaited assertions)
  5. Adds using TestTrackingDiagrams.Tracking; if not already present

Your source code stays unchanged — the wrapping is only applied to the compiled output.

Before (your source code):

response.StatusCode.Should().Be(HttpStatusCode.OK);
await result.Should().BeEquivalentToAsync(expected);

After (at compile time):

Track.That(() => response.StatusCode.Should().Be(HttpStatusCode.OK));
await Track.ThatAsync(async () => await result.Should().BeEquivalentToAsync(expected));

Suppressing Rewriting

You can selectively opt out of assertion wrapping using either attributes or pragma comments.

Attribute Suppression

Apply [SuppressAssertionTracking] to a method or class to skip all assertions within:

using TestTrackingDiagrams.Tracking;

[SuppressAssertionTracking]
public void HelperMethod()
{
    // Assertions in here won't be wrapped
    result.Should().NotBeNull();
}

[SuppressAssertionTracking]
public class InfrastructureTests
{
    // No assertions in this class are wrapped
}

Pragma Comment Suppression

Use // pragma:TrackAssertions:disable and // pragma:TrackAssertions:enable comments for finer-grained control — either inline (single statement) or as a range (multiple statements).

Single statement — add the pragma as a trailing comment:

x.Should().Be(1); // pragma:TrackAssertions:disable

Range — wrap a block of statements between disable/enable comments:

// pragma:TrackAssertions:disable
x.Should().Be(1);   // ← not wrapped
y.Should().Be(2);   // ← not wrapped
// pragma:TrackAssertions:enable

z.Should().Be(3);   // ← this one gets wrapped normally

Note: If you use // pragma:TrackAssertions:disable without a matching // pragma:TrackAssertions:enable, all subsequent assertions in that file will be skipped.

Integration with Project Templates

All project templates ship with the AssertionRewriter pre-configured — the [assembly: TrackAssertions] attribute and package reference are included by default.


Manual Tracking (Track.That())

If you can't use the AssertionRewriter (e.g. you use a different assertion library that doesn't have .Should()), you can manually wrap assertions with Track.That():

using TestTrackingDiagrams.Tracking;

[Fact]
public async Task Creates_order_and_returns_201()
{
    var response = await _client.PostAsJsonAsync("/orders", new { Item = "Widget", Qty = 3 });

    Track.That(() => response.StatusCode.Should().Be(HttpStatusCode.Created));

    var order = await response.Content.ReadFromJsonAsync<Order>();

    Track.That(() => order!.Item.Should().Be("Widget"));
    Track.That(() => order!.Qty.Should().Be(3));
}

This produces three green assertion notes in the sequence diagram:

  • ✓ Response status code should be Created
  • ✓ Order item should be Widget
  • ✓ Order qty should be 3

API

All methods live in the TestTrackingDiagrams.Tracking namespace on the static Track class.

Track.That(Action)

public static void That(
    Action assertion,
    [CallerArgumentExpression(nameof(assertion))] string? expression = null)

Executes the assertion and logs an inline note. On failure, logs a red note and re-throws the exception.

Track.That<T>(Func<T>)

public static T That<T>(
    Func<T> assertion,
    [CallerArgumentExpression(nameof(assertion))] string? expression = null)

Same as above but returns the value produced by the expression. Useful when you want to capture a result and assert on it in one step:

var count = Track.That(() => items.Should().HaveCount(5).And.Subject.Count);

Track.ThatAsync(Func<Task>)

public static async Task ThatAsync(
    Func<Task> assertion,
    [CallerArgumentExpression(nameof(assertion))] string? expression = null)

Async variant for assertions that involve await:

await Track.ThatAsync(async () =>
{
    var content = await response.Content.ReadAsStringAsync();
    content.Should().Contain("success");
});

Works With Any Assertion Library

Track.That() captures the expression text regardless of which assertion library you use:

// FluentAssertions/AwesomeAssertions
Track.That(() => result.Should().Be(42));

// xUnit Assert
Track.That(() => Assert.Equal(42, result));

// NUnit Assert
Track.That(() => Assert.That(result, Is.EqualTo(42)));

// Shouldly
Track.That(() => result.ShouldBe(42));

The AssertionExpressionFormatter produces the best output for FluentAssertions/AwesomeAssertions .Should(). patterns. For other libraries, it falls back to displaying the raw expression text (which is still readable and useful).


How It Works

  1. Expression capture: C# 10's [CallerArgumentExpression] captures the source text of the lambda passed to Track.That() at compile time.

  2. Formatting: AssertionExpressionFormatter transforms the raw expression (e.g. response.StatusCode.Should().Be(HttpStatusCode.Created)) into readable English (response status code should be Created).

  3. PlantUML injection: The formatted text is injected into the sequence diagram as a styled hnote across <<assertionNote>> via DefaultTrackingDiagramOverride.InsertPlantUml().

  4. Conditional styling: A <<assertionNote>> PlantUML style (smaller font, rounded corners) is only emitted when assertion notes are present in the diagram.


Expression Formatting

The formatter handles common FluentAssertions/AwesomeAssertions patterns:

Raw Expression Formatted Output
result.Count.Should().Be(3) Result count should be 3
order.Status.Should().Be(OrderStatus.Shipped) Order status should be Shipped
items.Should().HaveCount(5) Items should have count 5
response.Should().BeOfType<OrderResponse>() Response should be of type OrderResponse
list.Should().ContainSingle(x => x.IsAdmin) List should contain single x => x.IsAdmin
result.Should().NotBeNull() Result should not be null

Rules applied:

  • Splits on .Should(). — left side becomes the subject, right side becomes the predicate
  • PascalCase is split into space-separated words (HaveCounthave count)
  • Enum prefixes are stripped for simple arguments (HttpStatusCode.OKOK)
  • Generic type arguments are preserved (BeOfType<string>()be of type string)
  • .And. chains take only the first assertion
  • Lambdas in arguments are preserved as-is

Report Toggle

When any test in the report uses Track.That(), an Assertions: Show / Hide toggle appears in the report toolbar (alongside the existing Details and Headers toggles).

  • Default: Hidden — assertion notes are stripped from the diagram source before rendering
  • Show: Re-renders all diagrams with assertion notes visible
  • The toggle works at both report level (affects all diagrams) and scenario level (affects a single test)

No toggle when unused: If no tests in the report use Track.That(), the Assertions toggle is not shown.


Prerequisites

Track.That() resolves the current test identity using the following priority order:

  1. Track.TestIdResolver (static delegate — set automatically by framework integrations: LightBDD, BDDfy, ReqNRoll)
  2. TestIdentityScope.Current (AsyncLocal — set via TestIdentityScope.Begin())
  3. TestIdentityScope.GlobalFallback (static — useful in single-threaded test runners)

If none of these resolve a test ID, the assertion executes normally but no diagram note is logged. This means Track.That() is safe to use in shared helper methods that may run outside a test context.

Framework Integration (Automatic)

When using one of the framework adapter packages (TestTrackingDiagrams.LightBDD.*, TestTrackingDiagrams.BDDfy.*, TestTrackingDiagrams.ReqNRoll.*), Track.TestIdResolver is set up automatically during configuration — no manual wiring needed.

Custom Resolver

If you're using a framework that doesn't have a built-in adapter, set Track.TestIdResolver during test setup:

// Example: wire up xUnit v3 TestContext directly
Track.TestIdResolver = () => Xunit.TestContext.Current.Test?.UniqueID;

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally