Skip to content

Step Tracking

aryehcitron@gmail.com edited this page May 24, 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 Kronikol.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="Kronikol.StepTracking" Version="2.31.0" />
  1. Add the assembly-level opt-in attribute anywhere in your test project:
using Kronikol.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 Kronikol.StepTracking and Kronikol.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.


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

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


Conditional Step Bypass (SkipIf)

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.

Usage

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

Properties

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.

Supported Member Types

  • 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

Behavior

  • When SkipIf evaluates to true: the step is recorded as Bypassed (not Passed or Failed), the method body does not execute, and async methods return Task.CompletedTask.
  • When SkipIf evaluates to false: the step executes normally as if SkipIf were 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).

Report Rendering

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.

Examples

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

How It Works

The Kronikol.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
    IncludeTrackedAssertionsInStepList = true,   // Track.That() assertions appear as sub-steps (v2.33.56)
};

// InlineBackgroundSteps is a ReportConfigurationOptions property, not StepTrackingOptions.
// Set it on the options passed to CreateStandardReportsWithDiagrams():
// new ReportConfigurationOptions { InlineBackgroundSteps = false }
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. Note: This property belongs to ReportConfigurationOptions, not StepTrackingOptions — set it on the options passed to CreateStandardReportsWithDiagrams().

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.

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.

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 Kronikol adapter Requires Kronikol adapter
Assertion sub-steps With AssertionTracking Not supported Not supported
Async support Full async Full async Full async

Limitations

  • 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