-
Notifications
You must be signed in to change notification settings - Fork 1
Step Tracking
Added in v2.30.29
Step tracking lets you define BDD-style steps (Given/When/Then/But) as attributed methods. At build time, the TestTrackingDiagrams.StepTracking package uses IL weaving to automatically instrument these methods — recording their execution, parameters, timing, and pass/fail status in your test reports.
This gives you the expressive step structure of BDD frameworks without any framework dependency.
- Install the package in your test project:
<PackageReference Include="TestTrackingDiagrams.StepTracking" Version="2.31.0" />- Add the assembly-level opt-in attribute anywhere in your test project:
using TestTrackingDiagrams.Tracking;
[assembly: TrackSteps]- Decorate your step methods with BDD attributes:
public class OrderTests : TestBase
{
[GivenStep]
public void A_customer_exists() { /* setup logic */ }
[GivenStep]
public void The_customer_has_a_basket_with_items(int itemCount)
{
// setup logic with parameters
}
[WhenStep]
public void The_customer_places_an_order() { /* action */ }
[ThenStep]
public void An_order_confirmation_is_sent() { /* assertion */ }
[Fact]
public async Task Customer_can_place_order()
{
A_customer_exists();
The_customer_has_a_basket_with_items(3);
The_customer_places_an_order();
An_order_confirmation_is_sent();
}
}The test report will show structured steps:
| Keyword | Step | Status | Duration |
|---|---|---|---|
| Given | A customer exists | ✓ Passed | 2ms |
| And | The customer has a basket with items | ✓ Passed | 1ms |
| When | The customer places an order | ✓ Passed | 45ms |
| Then | An order confirmation is sent | ✓ Passed | 3ms |
Note: The
TrackStepsAttribute,GivenStepAttribute,WhenStepAttribute,ThenStepAttribute,ButStepAttribute,ButWhenStepAttribute, andStepAttributetypes are auto-generated into your project at build time — no manual type definitions needed.
| Attribute | Keyword | Phase Transition | Purpose |
|---|---|---|---|
[GivenStep] |
Given | → Setup | Preconditions, test data setup |
[WhenStep] |
When | → Action | The action under test |
[ThenStep] |
Then | → Action | Assertions and verification |
[ButStep] |
But | → Setup | Negative preconditions ("But the user is not an admin") |
[ButWhenStep] |
But | → Action | Negative continuations in the action phase ("But the retry fails") |
[Step] |
(none) | (none) | Generic step with no keyword |
All step attributes accept a Description property to override the humanized method name:
[GivenStep(Description = "A user with admin privileges")]
public void SetupAdminUser() { /* ... */ }Without Description, the method name is humanized automatically: SetupAdminUser → "Setup admin user".
Consecutive methods with the same keyword are automatically sequenced using And:
[GivenStep] public void A_user_exists() { } // → "Given"
[GivenStep] public void The_user_is_active() { } // → "And" (auto-sequenced)
[GivenStep] public void The_user_has_a_session() { } // → "And" (auto-sequenced)
[WhenStep] public void The_user_logs_out() { } // → "When" (new keyword resets)
[ThenStep] public void The_session_is_destroyed() { } // → "Then" (new keyword resets)
[ThenStep] public void A_logout_event_is_raised() { } // → "And" (auto-sequenced)The same sequencing applies to [ButStep] and [ButWhenStep]:
[ButStep] public void The_user_is_not_admin() { } // → "But"
[ButStep] public void The_user_has_no_mfa() { } // → "And" (auto-sequenced)[ButWhenStep] also displays "But" and sequences with [ButStep]:
[WhenStep] public void The_api_is_called() { } // → "When"
[ButWhenStep] public void The_retry_fails() { } // → "But" (transitions to Action phase)
[ButWhenStep] public void The_fallback_is_used() { } // → "And" (auto-sequenced)You never need to explicitly write "And" — it is always inferred from repetition.
Method parameters are automatically captured and displayed in the step report:
[GivenStep]
public void A_user_with_name_and_age(string name, int age)
{
// parameters "name" and "age" are captured at runtime
}
// Call:
A_user_with_name_and_age("Alice", 30);The report shows:
| Keyword | Step | Parameters |
|---|---|---|
| Given | A user with name and age | name="Alice", age="30" |
Both value types and reference types are captured. Value types are boxed for display.
Method names are converted to sentence-case prose using these rules:
| Method Name | Humanized Text |
|---|---|
TheUserLogsIn |
The user logs in |
A_User_Exists |
A user exists |
GetHTTPResponse |
Get http response |
Setup_Admin_User |
Setup admin user |
The algorithm:
- Replaces underscores with spaces
- Splits PascalCase at lowercase→uppercase boundaries
- Handles acronym sequences (e.g. "HTTP" stays together until the next word)
- Applies sentence case (first letter uppercase, rest lowercase)
When a method name starts with the same keyword as its attribute, the keyword prefix is automatically stripped to avoid duplication in reports:
| Attribute | Method Name | Step Text |
|---|---|---|
[GivenStep] |
GivenTheyGo |
They go |
[WhenStep] |
When_they_go |
They go |
[ThenStep] |
ThenItWorks |
It works |
[WhenStep] |
WheneverTheyGo |
Whenever they go |
[ButWhenStep] |
ButTheRetryFails |
The retry fails |
The match is whole-word only — WheneverTheyGo does not match because "Whenever" ≠ "When ". Similarly, ThenceTheyGo is not stripped because "Thence" ≠ "Then ".
[ButWhenStep] strips the "But" prefix (its display keyword), not "ButWhen".
This allows natural method naming conventions like GivenAUserExists() or When_the_api_is_called() without keyword repetition in the report output.
Step attributes automatically set the ambient Phase-Aware Tracking context:
-
[GivenStep]and[ButStep]→TestPhase.Setup -
[WhenStep],[ThenStep], and[ButWhenStep]→TestPhase.Action
This means your tracking extensions (SQL, HTTP, Redis, etc.) automatically adjust their verbosity based on which step is currently executing. Setup noise is reduced, while action-phase calls get full detail.
Phase transitions are enabled by default. To disable:
StepCollector.Options = new StepTrackingOptions { WhenTriggersAction = false };See Phase-Aware Tracking for details on how extensions respond to phase changes.
When Assertion Tracking is also enabled, assertions that execute within an active step are automatically recorded as sub-steps of that step:
[ThenStep]
public void The_order_total_is_correct()
{
_order.Total.Should().Be(99.99m); // ✓ recorded as sub-step
_order.Currency.Should().Be("GBP"); // ✓ recorded as sub-step
}The report shows:
| Keyword | Step | Sub-Steps |
|---|---|---|
| Then | The order total is correct | ✓ _order.Total.Should().Be(99.99m) |
| ✓ _order.Currency.Should().Be("GBP") |
This requires both TestTrackingDiagrams.StepTracking and TestTrackingDiagrams.AssertionTracking packages to be installed.
Other adapters: Assertion sub-steps also work inside ReqNRoll step definitions (v2.33.22+), LightBDD steps (v2.33.39+), and BDDfy steps (v2.33.23+) — any framework that brackets steps with
StepCollector.StartStep/CompleteStep.
Steps can be nested — calling a step method from within another step method creates a sub-step hierarchy:
[GivenStep]
public void A_complete_test_environment()
{
Setup_database();
Seed_test_data();
}
[Step]
public void Setup_database() { /* ... */ }
[Step]
public void Seed_test_data() { /* ... */ }Added in v2.33.46 | Restricted to ReqNRoll in v2.33.71
When multiple ReqNRoll scenarios within the same feature (or the same Rule) share a common prefix of steps, those steps are automatically extracted into a collapsible Background section in the report. This mirrors how Gherkin's Background: keyword works — the shared setup steps appear once in a dedicated section rather than being duplicated across every scenario.
Note: Background step detection only applies to ReqNRoll scenarios (v2.33.71+). Other frameworks (xUnit, TUnit, NUnit, LightBDD, BDDfy) do not use this feature.
The BackgroundStepsDetector runs after scenarios are mapped from each framework adapter (ReqNRoll, LightBDD, BDDfy, Step Tracking). It:
- Groups scenarios by their
Ruleproperty (scenarios outside any Rule form their own group). - Within each group containing ≥ 2 scenarios, finds the longest common prefix of steps (matching by keyword + text).
- Extracts those steps into
Scenario.BackgroundStepsand trims them fromScenario.Steps.
If a group has only one scenario, or the scenarios share no common prefix, no background is extracted.
Background steps render as a collapsed <details> section (labelled "Background") above the "Steps" section in each scenario. Click to expand and see the shared setup steps.
Different Rules within the same feature can have different background steps. For example:
-
Rule: Valid Orders — scenarios share
Given the system is running+And the user is authenticated -
Rule: Invalid Orders — scenarios share
Given the system is runningonly
Each Rule's background is detected independently.
If you use Gherkin Background: blocks in your .feature files, the background steps flow through to each scenario at the ReqNRoll level. The detector identifies them as a common prefix and extracts them into the Background section automatically — no additional configuration needed.
Added in v2.33.48
You can attach files (screenshots, logs, trace files, etc.) to the current step or scenario. Attachments appear as download links in the report.
Call Track.Attachment() from within a step method or test body:
[WhenStep]
public void I_take_a_screenshot()
{
var path = TakeScreenshot();
Track.Attachment(path, "Page Screenshot");
}
[ThenStep]
public void I_save_the_response()
{
File.WriteAllText("response.json", _responseBody);
Track.Attachment("response.json"); // name defaults to "response.json"
}| Parameter | Type | Description |
|---|---|---|
filePath |
string |
Path to the file to attach (absolute or relative). During report generation, the file is automatically copied into the Reports/attachments/ folder and the link is rewritten to a relative URL. |
name |
string? |
Display name for the attachment link. Defaults to the file name extracted from filePath. |
When called inside an active step, the attachment is associated with that step. When called outside any step (e.g. in test setup/teardown), it becomes a scenario-level attachment.
During report generation (CreateStandardReportsWithDiagrams), all attachment source files are automatically copied into Reports/attachments/ and their href values are rewritten to relative paths (e.g. attachments/openapi.json). This ensures attachment links work when reports are:
- Uploaded to GitHub Pages
- Published as CI artifacts
- Viewed from any location without the original source files
Files that don't exist on disk are left unchanged. Paths already pointing to attachments/ are not processed (to avoid double-copying from frameworks like LightBDD that manage their own attachment copies).
Duplicate file names from different source paths are automatically deduplicated (e.g. report.txt, report_2.txt).
The TestTrackingDiagrams.ReqNRoll.Core package includes an AttachmentCapturingPlugin that automatically intercepts calls to IReqnrollOutputHelper.AddAttachment(filePath). Any attachment added via the Reqnroll output helper is automatically captured by Track.Attachment() — no manual Track.Attachment() call needed.
This plugin is registered automatically via [assembly: RuntimePlugin] and requires no configuration.
The inner steps become sub-steps of the outer step in the report.
Added in v2.34.3
You can conditionally bypass a step at runtime based on a boolean property or field on the test class. When the condition is true, the step body is not executed — instead, the step is recorded as Bypassed with an optional reason.
public class PaymentTests : TestBase
{
// Property evaluated at runtime to decide whether to skip
protected bool PaymentGatewayUnavailable => !_gateway.IsHealthy;
[WhenStep(SkipIf = nameof(PaymentGatewayUnavailable), SkipReason = "Gateway down")]
private async Task WhenPaymentIsSubmitted()
{
// This body is NOT executed when PaymentGatewayUnavailable == true
await _client.PostAsync("/payments", _paymentRequest);
}
}| Property | Type | Description |
|---|---|---|
SkipIf |
string |
Name of a bool property or field on the test class (or its base classes). Use nameof() for compile-time safety. |
SkipReason |
string? |
Optional human-readable reason displayed in reports and diagrams when the step is bypassed. |
-
Instance properties with a getter returning
bool -
Static properties with a getter returning
bool -
Instance fields of type
bool -
Static fields of type
bool - Base class members — the weaver walks the inheritance chain to find the member
- When
SkipIfevaluates totrue: the step is recorded asBypassed(notPassedorFailed), the method body does not execute, and async methods returnTask.CompletedTask. - When
SkipIfevaluates tofalse: the step executes normally as ifSkipIfwere not set. - When the named member does not exist or is not
bool: a build warning is emitted and the step executes normally (no bypass logic injected).
Bypassed steps appear with a distinct status in the HTML report step list, and the BypassReason is displayed alongside the step text. In sequence diagrams, the step delimiter still appears (so the step's position in the flow is visible), but no HTTP calls or other tracked interactions are shown for it.
Feature flag pattern (base class):
public abstract class IntegrationTestBase
{
protected bool ExternalServicesDisabled { get; set; }
}
public class OrderTests : IntegrationTestBase
{
[WhenStep(SkipIf = nameof(ExternalServicesDisabled), SkipReason = "External services disabled in CI")]
public void Send_notification_email() { /* ... */ }
}Static property for environment-wide skip:
public class Tests
{
public static bool IsCI => Environment.GetEnvironmentVariable("CI") == "true";
[GivenStep(SkipIf = nameof(IsCI), SkipReason = "Skipped on CI")]
public void Warm_up_local_cache() { /* ... */ }
}The TestTrackingDiagrams.StepTracking NuGet package contains:
-
An MSBuild .targets file that auto-generates the step attribute source files into your project's intermediate output (
BeforeTargets="CoreCompile") -
An IL weaving task (
WeaveStepsTask) that runs after compilation (AfterTargets="CoreCompile") and modifies the compiled assembly
For each method decorated with a step attribute, the weaver:
- Builds
string[]andobject[]arrays from the method's parameters - Injects a call to
StepCollector.StartStep(keyword, text, paramNames, paramValues)at method entry - Wraps the original method body in a
try/catch:-
Success path: calls
StepCollector.CompleteStep(true, null) -
Exception path: calls
StepCollector.CompleteStep(false, ex.Message)then rethrows
-
Success path: calls
The weaving is guarded against double-application via a sentinel module attribute (__StepTrackingWeaved__).
StepCollector.Options = new StepTrackingOptions
{
PrependKeyword = true, // Include keyword in step display text
InlineParameters = true, // Show parameters inline with step text
WhenTriggersAction = true, // Given/But → Setup phase, When/Then → Action phase
ShowStepDelimiters = true, // Inject step delimiter bars into sequence diagrams
IncludeTrackedAssertionsInStepList = true, // Track.That() assertions appear as sub-steps (v2.33.56)
InlineBackgroundSteps = false // Show background steps inline instead of extracted (v2.33.71)
};| Option | Default | Description |
|---|---|---|
PrependKeyword |
true |
Include the keyword (Given/When/Then) in step display text |
InlineParameters |
true |
Show parameters inline with step text |
WhenTriggersAction |
true |
Step keywords trigger phase transitions (Given/But → Setup, When/Then → Action) |
ShowStepDelimiters |
true |
Inject step delimiter hnote bars into sequence diagrams at each top-level step |
IncludeTrackedAssertionsInStepList |
true |
When false, Track.That() assertions still appear in diagrams but are not added as sub-steps in the step list |
InlineBackgroundSteps |
false |
When true, background steps are shown inline within each scenario instead of being extracted into a separate Background section |
Remove the [assembly: TrackSteps] attribute or uninstall the package. Without the attribute, the weaver skips the assembly entirely.
Added in v2.30.35
When step tracking is active, each top-level BDD step injects a visual delimiter bar into the sequence diagram — a black hnote spanning the full width showing Step: Given/When/Then/And/But {text}. This makes it easy to see which HTTP calls, database queries, and other tracked interactions belong to which step.
Step delimiters are emitted by all step-aware integrations:
-
StepTracking IL weaver — automatically for
[GivenStep],[WhenStep],[ThenStep], etc. - BDDfy adapter (v2.33.23+) — automatically for each BDDfy step during execution
-
LightBDD adapter (v2.33.23+) — automatically via
StepTrackingStepDecorator
Delimiters are only injected for top-level steps; nested sub-steps do not produce additional delimiters.
When any test in the report contains step delimiters, a "Hide Steps" / "Show Steps" toggle button appears in the report toolbar (alongside the existing Headers and Assertions toggles).
- Default: Shown — delimiters are visible in diagrams
- Hide Steps: Re-renders all diagrams with delimiter bars stripped out
- 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 contain step delimiters, the Steps toggle is not shown.
Set ShowStepDelimiters = false to suppress delimiter injection while keeping step tracking active:
StepCollector.Options = new StepTrackingOptions
{
ShowStepDelimiters = false
};Step tracking works with all supported test frameworks:
-
xUnit v3 — Steps are collected via
StepCollectorand populated on theScenarioobject - TUnit — Same collection mechanism
- NUnit 4 — Same collection mechanism
-
BDDfy —
StepCollectorsteps are used when BDDfy's own Steps collection is empty (for inline tests) - LightBDD — LightBDD has its own step system; use that instead of this package
The step data flows into the Generated Reports HTML alongside the sequence diagrams.
| Feature | StepTracking | LightBDD | BDDfy |
|---|---|---|---|
| Step definition | Attributes | Attributes + runner | Fluent API + reflection |
| Framework dependency | None (IL weaving) | LightBDD runtime | BDDfy runtime |
| Parameter capture | Automatic (IL weaving) | Framework-provided | Method name convention |
| Phase integration | Automatic | Requires TTD adapter | Requires TTD adapter |
| Assertion sub-steps | With AssertionTracking | Not supported | Not supported |
| Async support | Full async | Full async | Full async |
-
Single test assembly: The
[assembly: TrackSteps]attribute must be present in each test assembly that uses step attributes.
- Assertion Tracking — Automatic assertion display in diagrams (can combine with step tracking for assertion sub-steps)
- Phase-Aware Tracking — How Setup/Action phases affect tracking verbosity
- Generated Reports — Where steps appear in the HTML report
- Internal Flow Tracking — Alternative approach for tracking internal method calls
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