Skip to content

Assertion Tracking

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

Added in v2.28.30

Track.That() captures assertion expressions and renders them as styled inline notes in your 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.


Quick Start

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");
});

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;

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).


Automatic Assertion Rewriting (AssertionRewriter)

Added in v2.29.16

The TestTrackingDiagrams.AssertionRewriter NuGet package eliminates the need to manually wrap every assertion in Track.That(). It uses a Roslyn-based MSBuild task that automatically rewraps .Should() expression statements at compile time.

Installation

<PackageReference Include="TestTrackingDiagrams.AssertionRewriter" Version="2.29.16" />

Then add the assembly-level opt-in attribute in any .cs file in your test project (commonly AssertionTracking.cs):

using TestTrackingDiagrams.Tracking;

[assembly: TrackAssertions]

How It Works

During compilation (before CoreCompile), the rewriter:

  1. Scans source files for the [assembly: TrackAssertions] attribute
  2. Finds all expression statements containing .Should() calls
  3. Wraps them in Track.That(() => ...) (or Track.ThatAsync(async () => ...) for awaited assertions)
  4. Adds using TestTrackingDiagrams.Tracking; if not already present
  5. Outputs rewritten files to the intermediate directory

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 with the [SuppressAssertionTracking] attribute:

using TestTrackingDiagrams.Tracking;

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

The attribute can be applied to methods or classes. The rewriter also respects #pragma warning disable regions.

Integration with Project Templates

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

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally