Table of Contents

Extensibility & Advanced Usage

This section dives below the surface: customizing execution, reporting, options, and integrating TinyBDD into enterprise test platforms.

Scenario Options

Configure when creating a context:

var ctx = Bdd.CreateContext(this, options: new ScenarioOptions {
    ContinueOnError = true,
    HaltOnFailedAssertion = false,
    MarkRemainingAsSkippedOnFailure = true,
    StepTimeout = TimeSpan.FromSeconds(5)
});
Option Effect Typical Use
ContinueOnError Non‑assert exceptions record failure but execution proceeds Collect multiple failures in a single diagnostic run
HaltOnFailedAssertion Re‑throw assertion (TinyBddAssertionException) immediately Fast‑fail style CI where first failure is enough
MarkRemainingAsSkippedOnFailure Tag remaining queued steps as skipped Preserve intent visibility without executing unsafe operations
StepTimeout Cancel any single step exceeding duration Guard external I/O or long integration steps

Pipeline Hooks

Pipeline.BeforeStep / AfterStep allow lightweight instrumentation (timing, logging, tracing). Adapters internally set loggers; you can customize by forking an adapter or exposing a factory.

// Pseudo: wrapping context creation to attach hooks
var ctx = Bdd.CreateContext(this);
var pipe = TinyReflection.GetPipeline(ctx); // hypothetical helper you add
pipe.BeforeStep = (c, meta) => _logger.LogDebug($"BEGIN {meta.Kind} {meta.Title}");
pipe.AfterStep  = (c, result) => _logger.LogInformation($"{result.Kind} {result.Title} [{(result.Error is null ? "OK" : "FAIL")}] {result.Elapsed.TotalMilliseconds:n0} ms");

Hooks run on the executing test thread—avoid heavy blocking operations.

Custom Reporters

Implement IBddReporter and feed it to GherkinFormatter.Write(ctx, reporter) after execution.

public sealed class JsonBddReporter : IBddReporter
{
    private readonly List<string> _lines = new();
    public void WriteLine(string message) => _lines.Add(message);
    public override string ToString() => JsonSerializer.Serialize(_lines);
}

var reporter = new JsonBddReporter();
GherkinFormatter.Write(ctx, reporter);
File.WriteAllText("scenario.json", reporter.ToString());

Format is entirely up to you (structured JSON, markdown, HTML, etc.).

For structured JSON reporting with full scenario/step metadata, use TinyBDD.Extensions.Reporting.

Observer Pattern

TinyBDD provides an observer pattern through IScenarioObserver and IStepObserver interfaces for adding cross-cutting concerns like telemetry, logging, and custom reporting without modifying core execution logic.

IScenarioObserver

Observe scenario-level lifecycle events:

public class LoggingScenarioObserver : IScenarioObserver
{
    private readonly ILogger _logger;

    public LoggingScenarioObserver(ILogger logger) => _logger = logger;

    public ValueTask OnScenarioStarting(ScenarioContext context)
    {
        _logger.LogInformation("Starting: {Feature} - {Scenario}", 
            context.FeatureName, context.ScenarioName);
        return default;
    }

    public ValueTask OnScenarioFinished(ScenarioContext context)
    {
        var passed = context.Steps.All(s => s.Error == null);
        _logger.LogInformation("Finished: {Feature} - {Scenario}, Passed: {Passed}", 
            context.FeatureName, context.ScenarioName, passed);
        return default;
    }
}

IStepObserver

Observe individual step lifecycle events:

public class MetricsStepObserver : IStepObserver
{
    private readonly IMetricsCollector _metrics;

    public MetricsStepObserver(IMetricsCollector metrics) => _metrics = metrics;

    public ValueTask OnStepStarting(ScenarioContext context, StepInfo step)
    {
        _metrics.IncrementCounter($"step.{step.Kind}.started");
        return default;
    }

    public ValueTask OnStepFinished(ScenarioContext context, StepInfo step, 
        StepResult result, StepIO io)
    {
        _metrics.RecordDuration($"step.{step.Kind}.duration", result.Elapsed);
        if (result.Error != null)
            _metrics.IncrementCounter($"step.{step.Kind}.failed");
        return default;
    }
}

Registering Observers

Use TinyBdd.Configure() to register observers:

var options = TinyBdd.Configure(builder => builder
    .AddObserver(new LoggingScenarioObserver(logger))
    .AddObserver(new MetricsStepObserver(metrics)));

var ctx = Bdd.CreateContext(this, options: options);

Use Cases

  • Telemetry: Send scenario/step metrics to Application Insights, DataDog, etc.
  • Distributed Tracing: Integrate with OpenTelemetry for distributed trace spans
  • Audit Logging: Record all scenario executions for compliance
  • Performance Monitoring: Track step durations and identify bottlenecks
  • Structured Reporting: Generate JSON reports for CI/CD (see Reporting Extension)

For complete JSON reporting capabilities, see TinyBDD.Extensions.Reporting.

Integrating With CI

  • Emit Gherkin output to standard test logs (adapters do this automatically).
  • Convert reporter output to build annotations (GitHub Actions ::notice, Azure DevOps logging commands) for fast triage.
  • Persist ScenarioContext.IO serialized artifacts for post‑failure analysis.

Custom Fluent Assertion Extensions

Extend FluentAssertion<T> with domain‑specific checks while preserving deferred semantics:

public static class DomainAssertionExtensions
{
    public static FluentAssertion<Order> ToHaveLineCount(this FluentAssertion<Order> a, int expected)
        => a.ToSatisfy(o => o.Lines.Count == expected, $"have {expected} line(s)");
}

Usage:

await Expect.For(order).ToHaveLineCount(2).ToSatisfy(o => o.Total > 0, "have positive total");

Composing Multi‑Subject Assertions

Sometimes you need to compare two evolving states. Capture both in a tuple or small record earlier, then assert on the composite.

await Given(() => (Original: GetUser(), Updated: UpdateUser()))
    .Then("email unchanged", t => t.Original.Email == t.Updated.Email)
    .And("version increments", t => t.Updated.Version == t.Original.Version + 1);

Cancellation Strategy

Every step shape has a CancellationToken variant. Inject tokens to wire scenarios into larger harness time budgets or global test cancellations.

await Given(ct => SeedAsync(ct)) // or Given("seed", (ct) => ...)
    .When("call api", (s, ct) => CallApiAsync(s, ct))
    .Then("status 200", r => r.Status == 200);

If a token cancels mid‑step, the pipeline records the step (Error = OperationCanceledException) then rethrows (unless you handle externally).

Timeboxed Steps

ScenarioOptions.StepTimeout wraps each step in a linked CTS. Your delegate should cooperate with cancellation; otherwise timeout occurs only at await boundaries that observe the token.

Partial Context Reconfiguration

Clone a context with tweaks without losing accumulated tags:

var ctx2 = Bdd.ReconfigureContext(ctx, proto => {
    proto.ScenarioName = proto.ScenarioName + " (retry)";
});

Use to model variant scenarios sharing feature metadata.

Working With Multiple Pipelines

You can run several scenarios in parallel using separate contexts. Because ambient isolation uses AsyncLocal, avoid setting Ambient.Current across threads you don't control; prefer explicit contexts for heavy parallel harnesses.

Mixing Unit & BDD Layers

For deep algorithmic units write classic unit tests. At the feature boundary, aggregate them through TinyBDD scenarios focused on what not how. This separation keeps BDD specs lean and stable through internal refactors.

Enterprise Hardening Checklist

Concern Recommendation
Flaky external dependencies Introduce test doubles; use StepTimeout for guard rails
Large data setup Encapsulate builders; keep Given cheap (< 10ms ideal)
Slow integration layers Mark long‑running scenarios with [Tag("slow")] for selective execution
Observability Add hooks or custom reporter emitting JSON; archive artifacts
Security sensitive data Avoid dumping secrets into IO / reporter output
Maintainability Enforce naming conventions in PR review (domain language)

Roadmap Ideas (Illustrative)

  • Async assertion library adapters
  • Built‑in JSON snapshot diffing helper
  • Rich HTML reporter (dark/light theme)
  • Visual timeline (Gantt) from step timings

If you build one of these, contribute back—TinyBDD intends to stay small, but high‑leverage, opt‑in modules are welcome.

Return to: Introduction