Skip to content

Integration xUnit3

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

Example project: A complete working example is available at examples/Example.Api/tests/Example.Api.Tests.Component.xUnit3/.


Overview

This guide walks you through integrating Kronikol with plain xUnit (no BDD framework). After completing this guide, your xUnit tests will automatically generate:

  • PlantUML sequence diagrams from HTTP traffic between your service and its dependencies
  • HTML reports with embedded diagrams
  • YAML specification files

This is the simplest integration path if you are already writing xUnit tests and just want to add automatic diagram generation.


Prerequisites

  • .NET 10.0 SDK or later
  • An ASP.NET Core API project to test (your "Service Under Test")
  • Basic familiarity with xUnit

Step 1: Create the Test Project

Create a new xUnit test project:

dotnet new xunit -n MyApi.Tests.Component

Step 2: Install NuGet Packages

dotnet add package Kronikol.xUnit3
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package xunit.v3
dotnet add package xunit.runner.visualstudio

Your <ItemGroup> should look like this:

<ItemGroup>
    <PackageReference Include="Kronikol.xUnit3" Version="2.31.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.12" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
    <PackageReference Include="xunit.v3" Version="1.0.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        <PrivateAssets>all</PrivateAssets>
    </PackageReference>
</ItemGroup>

Step 3: Create the Test Run Collection Fixture

xUnit uses "collection fixtures" to share state across tests. Kronikol provides DiagrammedTestRun as a base class for your collection fixture. This is where reports are generated.

Infrastructure/TestRun.cs:

using Kronikol;
using Kronikol.xUnit3;

namespace MyApi.Tests.Component.Infrastructure;

public class TestRun : DiagrammedTestRun, IDisposable
{
    public TestRun()
    {
        // Optional: start any HTTP fakes here
    }

    public void Dispose()
    {
        EndRunTime = DateTime.UtcNow;

        // Generate reports when the test run ends
        XUnitReportGenerator.CreateStandardReportsWithDiagrams(
            TestContexts,
            StartRunTime,
            EndRunTime,
            new ReportConfigurationOptions
            {
                SpecificationsTitle = "My API Specifications"
            });

        // Optional: dispose HTTP fakes here
    }
}

Step 4: Create the Test Collection Definition

Create a collection definition that ties all your diagrammed tests to the TestRun fixture:

DiagrammedTestCollection.cs:

using Kronikol.xUnit3;

namespace MyApi.Tests.Component;

[CollectionDefinition(DiagrammedComponentTest.DiagrammedTestCollectionName)]
public class DiagrammedTestCollection : ICollectionFixture<Infrastructure.TestRun> { }

This class is never instantiated directly — it just tells xUnit to create a single TestRun instance shared across all tests in the collection.


Step 5: Create the Base Fixture

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

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Kronikol.xUnit3;

namespace MyApi.Tests.Component.Infrastructure;

public abstract class BaseFixture : DiagrammedComponentTest
{
    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 XUnitTestTrackingMessageHandlerOptions
                {
                    CallerName = ServiceUnderTestName,
                    PortsToServiceNames =
                    {
                        { 80, ServiceUnderTestName },
                        { 5001, "Downstream Service A" }
                    }
                });
            });
        });
    }

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

    public override void Dispose()
    {
        Client?.Dispose();
        base.Dispose();
    }
}

Key points:

  • DiagrammedComponentTest is the library's base class. It applies [Collection("Diagrammed Test Collection")] automatically and enqueues the TestContext on Dispose() for report collection.
  • XUnitTestTrackingMessageHandlerOptions uses xUnit's built-in TestContext.Current to resolve the current test's identity.

Step 6: Write Test Scenarios

Tests are written as regular xUnit [Fact] or [Theory] methods. Use the [Endpoint] and [HappyPath] attributes to add metadata for the report.

Scenarios/Cake_Feature.cs:

using Kronikol.xUnit3;

namespace MyApi.Tests.Component.Scenarios;

[Endpoint("/cake")]
public partial class Cake_Feature
{
    [Fact]
    [HappyPath]
    public async Task Calling_Create_Cake_Endpoint_Returns_Cake()
    {
        await Given_a_valid_post_request_for_the_Cake_endpoint();
        await When_the_request_is_sent_to_the_cake_post_endpoint();
        await Then_the_response_should_be_successful();
    }

    [Fact]
    public async Task Calling_Create_Cake_Endpoint_Without_Eggs_Returns_Bad_Request()
    {
        await Given_a_valid_post_request_for_the_Cake_endpoint();
        await But_the_request_body_is_missing_eggs();
        await When_the_request_is_sent_to_the_cake_post_endpoint();
        await Then_the_response_http_status_should_be_bad_request();
    }
}

Scenarios/Cake_Feature.steps.cs:

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

namespace MyApi.Tests.Component.Scenarios;

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

    private async Task Given_a_valid_post_request_for_the_Cake_endpoint()
    {
        // Build your request using Client
    }

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

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

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

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

Key points:

  • [Endpoint("/cake")] — Sets the endpoint label for this feature group in the report.
  • [HappyPath] — Marks a scenario as a happy path (filterable in the HTML report).
  • Class names are converted to feature names: underscores become spaces (e.g. Cake_Feature → "Cake Feature").
  • Method names are converted to scenario names in the same way.

Step 7: Run the Tests

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

You can customise diagrams within a test using TrackingDiagramOverride:

using Kronikol.xUnit3;

// 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, HTTP calls made before StartAction() are wrapped in a visual "Setup" partition in the diagram.


Architecture Summary

┌─────────────────────────────────┐
│     DiagrammedTestCollection    │  ← Collection definition (one per assembly)
│   ICollectionFixture<TestRun>   │
└─────────────┬───────────────────┘
              │ creates once
              ▼
┌─────────────────────────────────┐
│           TestRun               │  ← Generates reports in Dispose()
│     : DiagrammedTestRun         │
└─────────────────────────────────┘
              │ shared across
              ▼
┌─────────────────────────────────┐
│          BaseFixture            │  ← Creates tracked HttpClient
│   : DiagrammedComponentTest     │     Enqueues TestContext on Dispose
└─────────────┬───────────────────┘
              │ inherited by
              ▼
┌─────────────────────────────────┐
│       Cake_Feature : BaseFixture│  ← Your test class with [Fact] methods
└─────────────────────────────────┘

Customisation Options

ReportConfigurationOptions

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 before StartAction() 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

XUnitTestTrackingMessageHandlerOptions

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

Faking Downstream Dependencies (Correctly)

When your SUT calls downstream HTTP services, those calls must flow through TestTrackingMessageHandler to produce proper HTTP-style diagram arrows (with method, status code, headers, body). Do not mock service client interfaces and use MessageTracker to manually log HTTP interactions — this produces event-style (blue) arrows that are misleading.

Recommended approaches:

See Tracking Dependencies#faking-dependencies-getting-proper-http-tracking for detailed examples of each approach.


Alternative: Assembly Fixture

If your project has multiple xUnit collections, a collection fixture will only capture tests in the "Diagrammed Test Collection" — tests in other collections are silently excluded from reports. xUnit v3's [assembly: AssemblyFixture] solves this by running the fixture once per assembly regardless of collections:

Global.cs:

[assembly: AssemblyFixture(typeof(MyApi.Tests.Component.Infrastructure.TestRun))]

Infrastructure/TestRun.cs:

using Kronikol;
using Kronikol.xUnit3;

namespace MyApi.Tests.Component.Infrastructure;

public class TestRun : DiagrammedTestRun, IDisposable
{
    public void Dispose()
    {
        EndRunTime = DateTime.UtcNow;

        XUnitReportGenerator.CreateStandardReportsWithDiagrams(
            TestContexts,
            StartRunTime,
            EndRunTime,
            new ReportConfigurationOptions
            {
                SpecificationsTitle = "My API Specifications"
            });
    }
}

With an assembly fixture, you still need test classes to enqueue their TestContext (either by inheriting from DiagrammedComponentTest or manually — see below), but you no longer need the DiagrammedTestCollection collection definition class or the [Collection] attribute.

When to use: Prefer assembly fixtures over collection fixtures when your project has multiple xUnit collections and you want all tests to contribute to the report.


Alternative: Manual TestContexts.Enqueue (No Base Class)

DiagrammedComponentTest does exactly one thing: call DiagrammedTestRun.TestContexts.Enqueue(TestContext.Current) on Dispose(). If your test classes already inherit from a shared fixture (e.g. a domain-specific base class or IClassFixture<T>) and you can't change the inheritance hierarchy, add the enqueue call directly to your existing fixture's teardown:

public class MyExistingBaseFixture : IDisposable
{
    // ... existing setup and test infrastructure ...

    public void Dispose()
    {
        // ... existing cleanup ...

        // This is the only line needed for Kronikol report collection
        DiagrammedTestRun.TestContexts.Enqueue(TestContext.Current);
    }
}

Or with IAsyncLifetime:

public class MyExistingBaseFixture : IAsyncLifetime
{
    public ValueTask InitializeAsync() => ValueTask.CompletedTask;

    public ValueTask DisposeAsync()
    {
        DiagrammedTestRun.TestContexts.Enqueue(TestContext.Current);
        return ValueTask.CompletedTask;
    }
}

Tip: Combine this with an assembly fixture (above) and you don't need [Collection("Diagrammed Test Collection")] on your test classes at all.


Troubleshooting

Reports folder is empty

  • Ensure TestRun.Dispose() calls XUnitReportGenerator.CreateStandardReportsWithDiagrams.
  • Ensure your test classes inherit from BaseFixture (which inherits from DiagrammedComponentTest).
  • Ensure you have the DiagrammedTestCollection collection definition class.

Tests are not showing in the report

  • Make sure each test class inherits from DiagrammedComponentTest (directly or via BaseFixture). The base class calls DiagrammedTestRun.TestContexts.Enqueue(TestContext.Current) on Dispose().

Most diagrams are missing / only a few scenarios appear in the report

If your SUT makes HTTP calls during startup (hosted services, health probes, warm-up requests), XUnitTestTrackingMessageHandlerOptions sets CurrentTestInfoFetcher to read from TestContext.Current.Test, which is null outside of test execution. As of v2.27.10, CurrentTestInfo.Fetcher is null-safe and returns a fallback value in this case. If you are on an older version, the fetcher threw a NullReferenceException that was caught internally — tests still passed, but the tracking handler stopped working for the affected request pipeline.

Fix (v2.27.9 and earlier): Upgrade to v2.27.10+. If you can't upgrade, use the base TestTrackingMessageHandlerOptions with a null-safe CurrentTestInfoFetcher delegate instead. See HTTP Tracking Setup for the pattern.

Empty specifications HTML / YAML

If any test has failed, the specifications files will be blank by design. The TestRunReport.html will still be generated.

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally