-
Notifications
You must be signed in to change notification settings - Fork 1
Assertion Tracking
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.
Added in v2.30.7
The recommended approach is the Kronikol.AssertionTracking package — an MSBuild task that automatically instruments your .Should() and Assert.That() assertions at compile time via IL weaving. No changes to your test code required.
Requirements: Works with FluentAssertions, AwesomeAssertions, and TUnit assertion libraries (i.e. assertions using the
.Should()orAssert.That()API). If you use a different assertion library, see Manual Tracking below.
Compatibility: The weaver operates as an MSBuild task that instruments compiled IL after compilation (
AfterTargets="CoreCompile"). It works with any .NET SDK version (the task targets netstandard2.0). 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.
- Install the package:
<PackageReference Include="Kronikol.AssertionTracking" Version="2.31.0" />- Add the assembly-level opt-in attribute anywhere in your test project (commonly in a
GlobalUsings.csorAssertionTracking.csfile):
using Kronikol.Tracking;
[assembly: TrackAssertions]The
TrackAssertionsAttributeandSuppressAssertionTrackingAttributetypes 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
}The MSBuild integration runs in two phases:
-
Before compilation (
BeforeTargets="CoreCompile"): Auto-generates theTrackAssertionsAttributeandSuppressAssertionTrackingAttributetypes into your project's intermediate output directory. -
After compilation (
AfterTargets="CoreCompile"): TheWeaveAssertionsTaskopens the compiled assembly with Mono.Cecil, finds all methods containing assertion call chains, and wraps each assertion statement in:
try {
[original assertion IL]
Track.AssertionPassedWithValues(expression, varNames, varValues, filePath, lineNumber)
} catch (Exception ex) {
Track.AssertionFailedWithValues(expression, ex.Message, varNames, varValues, filePath, lineNumber)
rethrow
}
The expression parameter is read from the original source file using PDB sequence point information — so your assertion notes display the actual source text, not decompiled IL.
Your source code stays unchanged — the instrumentation is only applied to the compiled output.
You can selectively opt out of assertion tracking using either attributes or pragma comments.
Apply [SuppressAssertionTracking] to a method or class to skip all assertions within:
using Kronikol.Tracking;
[SuppressAssertionTracking]
public void HelperMethod()
{
// Assertions in here won't be instrumented
result.Should().NotBeNull();
}
[SuppressAssertionTracking]
public class InfrastructureTests
{
// No assertions in this class are instrumented
}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:disableRange — wrap a block of statements between disable/enable comments:
// pragma:TrackAssertions:disable
x.Should().Be(1); // ← not instrumented
y.Should().Be(2); // ← not instrumented
// pragma:TrackAssertions:enable
z.Should().Be(3); // ← this one gets instrumented normallyNote: If you use
// pragma:TrackAssertions:disablewithout a matching// pragma:TrackAssertions:enable, all subsequent assertions in that file will be skipped.
The IL weaver handles null-conditional operators transparently:
// This works correctly — no warnings, no workarounds needed
order?.Status.Should().Be("Shipped");When order is null, the ?. operator's IL branch is retargeted to exit the try block cleanly via leave. No assertion is tracked (correct — .Should() was never called), and no InvalidProgramException or NullReferenceException is thrown.
Added in v2.30.12
The IL weaver captures the runtime values of variables used as assertion arguments and substitutes them into the diagram note text. For example:
var expected = HttpStatusCode.OK;
response.StatusCode.Should().Be(expected);Instead of showing response status code should be expected in the diagram, the note displays:
✓ response status code should be 'OK'
The weaver detects variable loads (both regular locals and async state machine fields) that occur after the .Should() call — these are the arguments to assertion methods like .Be(), .BeInRange(), etc. At runtime, the captured values are formatted using these rules:
-
null→'null' - Strings → displayed as-is (truncated at 100 characters)
- Small scalar collections (≤10 items) → inline values, e.g.
'[ 1, 2, 3 ]','[ "Milk", "Sugar" ]','[ Monday, Friday ]' - Large collections (>10 items) or collections with complex objects →
'[N items]' - Empty collections →
'[0 items]' - Other objects →
ToString()result (if meaningful; type-name-only results are suppressed)
Scalar types that qualify for inline display: primitives (int, bool, double, etc.), string, enum, decimal, Guid, DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly. Strings are quoted in inline format; other scalars are unquoted. Null items within collections display as null.
Dotted property chain walking (up to 3 levels): When the expression references a property path like config.MaxRetries, the weaver captures the root object (config) and resolves the property chain via reflection at runtime.
When all assertion arguments are constants (e.g. .Be(42)), no variable capture is emitted — the zero-overhead Track.AssertionPassed path is used instead.
Added in v2.30.13
Variables captured inside lambda predicates are also resolved. This is common with FluentAssertions collection assertions:
var expectedId = "abc";
var list = new List<Item> { new Item { Id = "abc" } };
list.Should().Contain(x => x.Id == expectedId);Instead of showing expectedId in the diagram note, the note displays the resolved value 'abc'.
This works for:
- Single captured variables:
x => x.Id == expectedId - Multiple captured variables:
x => x.Id == id && x.Name == name - OR conditions:
x => x.Id == id1 || x.Id == id2 - Async method closures (variables captured across
awaitboundaries) -
Instance fields in lambda predicates (v2.31.1):
l => l.EntityId == _orderId && l.Action == "Created"— the weaver resolves_orderIdto its runtime value for assertions that useExpression<Func<T, bool>>predicates (e.g.,.Contain(),.ContainSingle(),.OnlyContain()).
The weaver detects compiler-generated display class fields (<>c__DisplayClass), expression tree field references (ldtoken + Expression.Field()), and lambda body field access for captured variable resolution.
Added in v2.30.32
The weaver fully supports assertions in async methods, including:
-
Synchronous assertions in async methods:
response.StatusCode.Should().Be(HttpStatusCode.OK);— instrumented normally. -
Awaited assertions:
await act.Should().ThrowAsync<InvalidOperationException>();— the weaver detects theGetAwaiter()/GetResult()pattern and wraps the entire await expression including the merge point where the failure manifests.
To disable IL weaving entirely (e.g. for debugging), set the MSBuild property in your .csproj:
<PropertyGroup>
<TrackAssertionsEnabled>false</TrackAssertionsEnabled>
</PropertyGroup>All project templates ship with assertion tracking pre-configured — the [assembly: TrackAssertions] attribute and package reference are included by default.
-
Non-supported assertion libraries: Only
.Should()on FluentAssertions/AwesomeAssertions types andAssert.That()on TUnit types are detected. For other assertion libraries, useTrack.That()manually. - IntelliSense: Since the weaver runs post-compilation, IntelliSense and IDE analysis see your unmodified code — this is a feature (no noise), but means assertion tracking won't show in design-time analysis.
If you use an assertion library that isn't automatically detected (e.g. xUnit Assert, NUnit Assert, Shouldly), you can manually wrap assertions with Track.That():
using Kronikol.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
All methods live in the Kronikol.Tracking namespace on the static Track class.
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.
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);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");
});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).
Wrapping assertions in lambdas with Track.That() can introduce nullable analysis warnings when your assertion uses the null-conditional operator (?.) or involves nullable reference types. This is because C#'s nullable flow analysis doesn't propagate null-state information into lambdas.
Options:
-
Use
!instead of?.(recommended):Track.That(() => order!.Status.Should().Be("Shipped"));
-
Suppress inline:
#pragma warning disable CS8602 Track.That(() => order?.Status.Should().Be("Shipped")); #pragma warning restore CS8602
-
Use automatic tracking instead — the IL weaver handles
?.natively without nullable warnings.
Tip: If you're using the automatic
[assembly: TrackAssertions]approach, nullable warnings from assertion wrapping don't apply — the IL weaver preserves the original null-propagation semantics.
-
Expression capture: C# 10's
[CallerArgumentExpression]captures the source text of the lambda passed toTrack.That()at compile time. For IL-weaved assertions, the expression is read from the source file via PDB sequence points. -
Formatting:
AssertionExpressionFormattertransforms the raw expression (e.g.response.StatusCode.Should().Be(HttpStatusCode.Created)) into readable English (response status code should be Created). -
PlantUML injection: The formatted text is injected into the sequence diagram as a styled
hnote across <<assertionNote>>viaDefaultTrackingDiagramOverride.InsertPlantUml(). -
Conditional styling: A
<<assertionNote>>PlantUML style (smaller font, rounded corners) is only emitted when assertion notes are present in the diagram.
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 (
HaveCount→have count) - Enum prefixes are stripped for simple arguments (
HttpStatusCode.OK→OK) - 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
When any test in the report uses assertion tracking, a "Show Assertions" / "Hide Assertions" toggle button appears in the report toolbar (alongside the existing Headers and Steps toggles).
- Default: Hidden — assertion notes are stripped from the diagram source before rendering
- Show Assertions: 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 assertion tracking, the Assertions toggle is not shown.
Track.That() and the IL weaver both resolve the current test identity using the following priority order:
-
Track.TestIdResolver(static delegate — set automatically by framework integrations: LightBDD, BDDfy, ReqNRoll) -
TestIdentityScope.Current(AsyncLocal — set viaTestIdentityScope.Begin()) -
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 assertion tracking is safe to use in shared helper methods that may run outside a test context.
When using one of the framework adapter packages (Kronikol.LightBDD.*, Kronikol.BDDfy.*, Kronikol.ReqNRoll.*), Track.TestIdResolver is set up automatically during configuration — no manual wiring needed.
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;- Step Tracking — BDD-style step attributes with automatic assertion sub-step integration
- Phase-Aware Tracking — How Setup/Action phases affect tracking verbosity
Getting Started
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration ClickHouse Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Search Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
- Merging Parallel Reports
Reference