Skip to content

Integration LightBDD TUnit

aryehcitron@gmail.com edited this page May 24, 2026 · 11 revisions

Integration Guide: LightBDD with TUnit

Example project: A complete working example is available at examples/Example.Api/tests/Example.Api.Tests.Component.LightBDD.TUnit/. You can reference it alongside this guide for a fully working implementation.


Overview

This guide walks you through integrating Kronikol with LightBDD using TUnit as the test runner. After completing this guide, your LightBDD tests will automatically generate:

  • PlantUML sequence diagrams from HTTP traffic between your service and its dependencies
  • HTML reports with embedded diagrams (integrated into LightBDD's report pipeline)
  • YAML specification files

LightBDD is a BDD framework that lets you write scenarios as C# method calls (given => ..., when => ..., then => ...) using its Runner.RunScenarioAsync pattern, with support for composite steps, tabular data, and rich reporting.

Using xUnit instead of TUnit? See Integration LightBDD xUnit3 or Integration LightBDD xUnit2.


Prerequisites

  • .NET 8.0 SDK or later
  • An ASP.NET Core API project to test (your "Service Under Test")
  • Basic familiarity with LightBDD and TUnit

Step 1: Create the Test Project

Create a new console project (TUnit requires OutputType=Exe):

dotnet new console -n MyApi.Tests.Component.LightBDD

Add <OutputType>Exe</OutputType> to your .csproj:

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <IsTestProject>true</IsTestProject>
</PropertyGroup>

Step 2: Install NuGet Packages

dotnet add package Kronikol.LightBDD.TUnit
dotnet add package LightBDD.TUnit

Your <ItemGroup> should look like this:

<ItemGroup>
    <PackageReference Include="Kronikol.LightBDD.TUnit" Version="2.31.0" />
    <PackageReference Include="LightBDD.TUnit" Version="3.12.0" />
</ItemGroup>

Note: TUnit uses Microsoft.Testing.Platform (not Microsoft.NET.Test.Sdk). The LightBDD.TUnit package handles the test runner configuration automatically. You do not need to install TUnit, xunit.v3, or Microsoft.NET.Test.Sdk separately.


Step 3: Create the LightBDD Scope Configuration

LightBDD for TUnit uses a LightBddScopeAttribute to configure the test run. Create Infrastructure/ConfiguredLightBddScope.cs:

using LightBDD.Core.Configuration;
using LightBDD.Framework.Configuration;
using LightBDD.TUnit;
using Kronikol;
using Kronikol.LightBDD.TUnit;

[assembly: ConfiguredLightBddScope]

namespace MyApi.Tests.Component.LightBDD.Infrastructure;

public class ConfiguredLightBddScopeAttribute : LightBddScopeAttribute
{
    protected override void OnConfigure(LightBddConfiguration configuration)
    {
        // Wire up Kronikol report generation into LightBDD's report pipeline
        // Note: The legacy configuration.ReportWritersConfiguration().CreateStandardReportsWithDiagrams()
        // overload still works but is deprecated.
        configuration.CreateStandardReportsWithDiagrams(
            new ReportConfigurationOptions
            {
                SpecificationsTitle = "My API Specifications"
            });

        // Optional: Register global setup/teardown for HTTP fakes
        configuration.ExecutionExtensionsConfiguration()
            .RegisterGlobalTearDown("dispose factory", BaseFixture.DisposeFactory)
            .RegisterGlobalSetUp("http fakes", StartHttpFakes, DisposeHttpFakes);
    }

    private void StartHttpFakes() { /* start your HTTP fakes here */ }
    private void DisposeHttpFakes() { /* dispose your HTTP fakes here */ }
}

Key points:

  • [assembly: ConfiguredLightBddScope] is required — it registers TUnit assembly hooks that initialise and finalise LightBDD.
  • The scope class extends LightBddScopeAttribute (which uses TUnit's [Before(Assembly)]/[After(Assembly)] hooks internally).
  • CreateStandardReportsWithDiagrams hooks into LightBDD's native report pipeline, so reports are generated automatically when the test run ends.

Note: The LightBddConfiguration.CreateStandardReportsWithDiagrams() overload (called directly on LightBddConfiguration rather than on ReportWritersConfiguration()) also registers a TUnitArgumentCaptureDecorator that automatically captures raw test method arguments for rich sub-table rendering of complex objects. To use it, call configuration.CreateStandardReportsWithDiagrams(options) instead of configuration.ReportWritersConfiguration().CreateStandardReportsWithDiagrams(options).


Step 4: Create the Base Fixture

Create Infrastructure/BaseFixture.cs. All your test classes will inherit from this:

using LightBDD.TUnit;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Kronikol.LightBDD.TUnit;

namespace MyApi.Tests.Component.LightBDD.Infrastructure;

public abstract class BaseFixture : FeatureFixture, IDisposable
{
    private static readonly WebApplicationFactory<Program>? SFactory;
    protected HttpClient Client { get; }

    private const string ServiceUnderTestName = "My API";

    static BaseFixture()
    {
        SFactory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.TrackDependenciesForDiagrams(new LightBddTestTrackingMessageHandlerOptions
                {
                    CallerName = ServiceUnderTestName,
                    PortsToServiceNames =
                    {
                        { 80, ServiceUnderTestName },
                        { 5001, "Downstream Service A" }
                    }
                });
            });
        });
    }

    protected BaseFixture()
    {
        Client = SFactory!.CreateTestTrackingClient(
            new LightBddTestTrackingMessageHandlerOptions
            {
                FixedNameForReceivingService = ServiceUnderTestName
            });
    }

    public void Dispose() => Client.Dispose();
    public static void DisposeFactory() => SFactory?.Dispose();
}

Key points:

  • The static constructor creates the WebApplicationFactory once for all tests.
  • Each test instance gets its own HttpClient via the instance constructor.
  • The test inherits from FeatureFixture (LightBDD's base class for TUnit).
  • LightBddTestTrackingMessageHandlerOptions automatically resolves the current test context using LightBDD's ScenarioExecutionContext.

Step 5: Write a Scenario Feature Class

LightBDD uses partial classes — one file for the scenario definitions, one for the step implementations.

Scenarios/Cake_Feature.cs (scenario definitions):

using LightBDD.Framework.Scenarios;
using Kronikol.LightBDD.TUnit;

namespace MyApi.Tests.Component.LightBDD.Scenarios;

[FeatureDescription("/cake")]
public partial class Cake_Feature
{
    [HappyPath]
    [Scenario]
    public async Task Calling_Create_Cake_Endpoint_Successfully()
    {
        await Runner.RunScenarioAsync(
            given => A_valid_post_request_for_the_Cake_endpoint(),
            when => The_request_is_sent_to_the_cake_post_endpoint(),
            then => The_response_should_be_successful());
    }

    [Scenario]
    public async Task Calling_Create_Cake_Endpoint_Without_Eggs()
    {
        await Runner.RunScenarioAsync(
            given => A_valid_post_request_for_the_Cake_endpoint(),
            but => The_request_body_is_missing_eggs(),
            when => The_request_is_sent_to_the_cake_post_endpoint(),
            then => The_response_http_status_should_be_bad_request());
    }
}

Scenarios/Cake_Feature.steps.cs (step implementations):

using System.Net;
using System.Net.Http.Json;
using MyApi.Tests.Component.LightBDD.Infrastructure;

namespace MyApi.Tests.Component.LightBDD.Scenarios;

public partial class Cake_Feature : BaseFixture
{
    private HttpResponseMessage? _response;

    private async Task A_valid_post_request_for_the_Cake_endpoint()
    {
        // Set up your request data using Client
    }

    private async Task The_request_body_is_missing_eggs()
    {
        // Modify request
    }

    private async Task The_request_is_sent_to_the_cake_post_endpoint()
    {
        _response = await Client.PostAsJsonAsync("cake", /* your request */);
    }

    private async Task The_response_should_be_successful()
    {
        _response!.StatusCode.Should().Be(HttpStatusCode.OK);
    }

    private async Task The_response_http_status_should_be_bad_request()
    {
        _response!.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }
}

Key points:

  • [FeatureDescription("/cake")] sets the endpoint label in the report.
  • [HappyPath] marks a scenario as a happy path (from Kronikol.LightBDD.TUnit).
  • [Scenario] is an alias for TUnit's [Test] attribute provided by LightBDD.TUnit.
  • Steps are regular async Task methods — method names are converted to readable text by LightBDD (underscores become spaces).

Step 6: Run the Tests

Since TUnit uses Microsoft.Testing.Platform, you can run tests with:

dotnet run

Or:

dotnet test

After the tests complete, check the bin/Debug/net10.0/Reports/ folder:

File Description
Specifications.html HTML specifications with embedded PlantUML sequence diagrams
TestRunReport.html HTML test run report with diagrams and execution summary
Specifications.yml YAML specifications

Using PlantUML Overrides

LightBDD's TUnit adapter provides a TrackingDiagramOverride class for customising diagrams within a test:

using Kronikol.LightBDD.TUnit;

// Insert a delimiter between multiple requests in the diagram
TrackingDiagramOverride.InsertTestDelimiter("Step 1");

// Insert raw PlantUML markup
TrackingDiagramOverride.InsertPlantUml("note over MyApi : Custom note");

// Override the start/end of diagram generation
TrackingDiagramOverride.StartOverride();
TrackingDiagramOverride.EndOverride();

// Explicitly mark the boundary between setup and action phases
TrackingDiagramOverride.StartAction();

Setup separation: When SeparateSetup = true is set on ReportConfigurationOptions, LightBDD automatically detects the boundary between GIVEN steps and WHEN/THEN steps. HTTP calls made during GIVEN steps are wrapped in a visual "Setup" partition in the diagram — no manual StartAction() call is needed.

Tip: InsertTestDelimiter is particularly useful when using TabularAttributes, where a single scenario runs multiple iterations. Insert a delimiter between each iteration to clearly separate them in the diagram.


Customisation Options

ReportConfigurationOptions

Passed to CreateStandardReportsWithDiagrams:

Property Default Description
SpecificationsTitle "Service Specifications" Title shown at the top of reports
PlantUmlServerBaseUrl "https://plantuml.com/plantuml" PlantUML server URL
HtmlSpecificationsFileName "Specifications" Output filename for specs HTML
HtmlTestRunReportFileName "TestRunReport" Output filename for test run HTML
YamlSpecificationsFileName "Specifications" Output filename for YAML specs
HtmlSpecificationsCustomStyleSheet Stylesheets.VioletThemeStyleSheet Custom CSS appended to specs HTML
ExcludedHeaders [] HTTP headers to exclude from diagrams
SeparateSetup false When true, HTTP calls made during GIVEN steps are wrapped in a visual "Setup" partition in the diagram
HighlightSetup true When true (and SeparateSetup is enabled), the setup partition is rendered with a background colour

LightBddTestTrackingMessageHandlerOptions

Property Description
CallerName Display name for the service making outgoing HTTP calls
FixedNameForReceivingService Display name for the service receiving requests (your SUT)
PortsToServiceNames Dictionary mapping port numbers to friendly service names. Unmapped ports appear as localhost_80, localhost_5001, etc.

Differences from LightBDD + xUnit v3

If you're migrating from the Kronikol.LightBDD.xUnit3 package, the key changes are:

Aspect xUnit v3 TUnit
Package Kronikol.LightBDD.xUnit3 Kronikol.LightBDD.TUnit
LightBDD package LightBDD.XUnit3 LightBDD.TUnit
Test runner xUnit v3 TUnit (Microsoft.Testing.Platform)
Scope setup [assembly: TestPipelineStartup(typeof(...))] [assembly: ConfiguredLightBddScope]
Scope base class LightBddScope (class) LightBddScopeAttribute (attribute)
Test SDK Microsoft.NET.Test.Sdk + xunit.v3 Not needed (TUnit handles this)
Namespace Kronikol.LightBDD.xUnit3 Kronikol.LightBDD.TUnit

All other concepts (FeatureFixture, Runner.RunScenarioAsync, CompositeStep, TrackingDiagramOverride, etc.) remain the same.


Troubleshooting

Reports are not generated

  • Ensure [assembly: ConfiguredLightBddScope] (or your custom scope attribute) is present at the top of your scope file.
  • Ensure CreateStandardReportsWithDiagrams is called in OnConfigure.
  • Ensure your csproj has <OutputType>Exe</OutputType> — TUnit requires executable output.

Empty specifications HTML / YAML

If any test has failed, the specifications files will be blank by design (they only generate on a fully passing test run). The TestRunReport.html will still be generated.

CreateStandardReportsWithDiagrams requires a testCountResolver parameter

If the compiler is asking you for a Func<Assembly, int> testCountResolver parameter, you are importing the wrong namespace. The core namespace Kronikol.LightBDD exposes a lower-level overload that requires this parameter. The framework-specific namespace provides an overload that supplies it automatically:

Wrong namespace Correct namespace
using Kronikol.LightBDD; using Kronikol.LightBDD.TUnit;

The framework-specific overload internally calls assembly.CountNumberOfTestsInAssembly() to count [Scenario] methods in your test assembly. This count is used to detect when all tests have completed so reports are written only once at the end of the run, rather than being overwritten after each individual test.

[Scenario] attribute not found

[Scenario] is an alias for TUnit's [Test] attribute, provided by LightBDD.TUnit via a global using in its .props file. Ensure your project references LightBDD.TUnit (either directly or via Kronikol.LightBDD.TUnit).

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally