Skip to content

Step Tracking

Aryeh Citron edited this page May 11, 2026 · 11 revisions

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.


Quick Start

  1. Install the package in your test project:
<PackageReference Include="TestTrackingDiagrams.StepTracking" Version="2.31.0" />
  1. Add the assembly-level opt-in attribute anywhere in your test project:
using TestTrackingDiagrams.Tracking;

[assembly: TrackSteps]
  1. 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, and StepAttribute types are auto-generated into your project at build time — no manual type definitions needed.


Available Attributes

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

Description Override

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


Keyword Sequencing

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.


Parameter Capture

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 Name Humanization

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:

  1. Replaces underscores with spaces
  2. Splits PascalCase at lowercase→uppercase boundaries
  3. Handles acronym sequences (e.g. "HTTP" stays together until the next word)
  4. Applies sentence case (first letter uppercase, rest lowercase)

Keyword Deduplication

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.


Phase Transitions

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.


Assertion Sub-Steps

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.


Nested Steps

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() { /* ... */ }

Background Steps

Added in v2.33.46

When multiple 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.

How It Works

The BackgroundStepsDetector runs after scenarios are mapped from each framework adapter (ReqNRoll, LightBDD, BDDfy, Step Tracking). It:

  1. Groups scenarios by their Rule property (scenarios outside any Rule form their own group).
  2. Within each group containing ≥ 2 scenarios, finds the longest common prefix of steps (matching by keyword + text).
  3. Extracts those steps into Scenario.BackgroundSteps and trims them from Scenario.Steps.

If a group has only one scenario, or the scenarios share no common prefix, no background is extracted.

Report Rendering

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.

Rule-Scoped Backgrounds

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 running only

Each Rule's background is detected independently.

BDD Frameworks (ReqNRoll / Gherkin)

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.


File Attachments

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.

Track.Attachment()

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.

Automatic File Copying

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

ReqNRoll Automatic Capture

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.


How It Works

The TestTrackingDiagrams.StepTracking NuGet package contains:

  1. An MSBuild .targets file that auto-generates the step attribute source files into your project's intermediate output (BeforeTargets="CoreCompile")
  2. 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:

  1. Builds string[] and object[] arrays from the method's parameters
  2. Injects a call to StepCollector.StartStep(keyword, text, paramNames, paramValues) at method entry
  3. 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

The weaving is guarded against double-application via a sentinel module attribute (__StepTrackingWeaved__).


Configuration

StepTrackingOptions

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

Disabling Step Tracking

Remove the [assembly: TrackSteps] attribute or uninstall the package. Without the attribute, the weaver skips the assembly entirely.


Step Delimiters in Sequence Diagrams

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.

Delimiters are only injected for top-level steps; nested sub-steps do not produce additional delimiters.

Report Toggle

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.

Disabling Delimiters

Set ShowStepDelimiters = false to suppress delimiter injection while keeping step tracking active:

StepCollector.Options = new StepTrackingOptions
{
    ShowStepDelimiters = false
};

Framework Compatibility

Step tracking works with all supported test frameworks:

  • xUnit v3 — Steps are collected via StepCollector and populated on the Scenario object
  • TUnit — Same collection mechanism
  • NUnit 4 — Same collection mechanism
  • BDDfyStepCollector steps 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.


Comparison with BDD Frameworks

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

Limitations

  • No conditional steps: All decorated methods are always instrumented. Use [Step] with Description for generic steps that don't fit Given/When/Then.
  • Single test assembly: The [assembly: TrackSteps] attribute must be present in each test assembly that uses step attributes.

See Also

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally