-
Notifications
You must be signed in to change notification settings - Fork 1
Integration xUnit2
Example project: A complete working example is available at
examples/Example.Api/tests/Example.Api.Tests.Component.xUnit2/.
This guide walks you through integrating Kronikol with xUnit v2 (no BDD framework). After completing this guide, your xUnit v2 tests will automatically generate:
- PlantUML sequence diagrams from HTTP traffic between your service and its dependencies
- HTML reports with embedded diagrams
- YAML specification files
xUnit v2 does not have TestContext.Current (introduced in xUnit v3), so this integration uses AsyncLocal<T> and a BeforeAfterTestAttribute to track the current test identity. A custom XunitTestFramework is used to capture test results and generate reports after all tests complete.
- .NET 10.0 SDK or later
- An ASP.NET Core API project to test (your "Service Under Test")
- Basic familiarity with xUnit v2
Create a new xUnit v2 test project:
dotnet new xunit -n MyApi.Tests.Componentdotnet add package Kronikol.xUnit2
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package xunit
dotnet add package xunit.runner.visualstudioYour <ItemGroup> should look like this:
<ItemGroup>
<PackageReference Include="Kronikol.xUnit2" 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" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>xUnit v2's testhost calls Environment.Exit when tests finish, giving ProcessExit only ~2 seconds — not enough time for report generation. The library provides a custom XunitTestFramework that generates reports before the testhost exits.
global using Xunit;
[assembly: TestFramework(
"Kronikol.xUnit2.ReportingTestFramework",
"Kronikol.xUnit2")]This also captures test results (pass/fail/skip) automatically via a message sink wrapper.
Even though the custom framework handles report generation, a collection fixture is still useful for:
- Starting/stopping HTTP fakes
- Configuring
ReportConfigurationOptions
using Kronikol;
using Kronikol.xUnit2;
namespace MyApi.Tests.Component.Infrastructure;
public class TestRun : DiagrammedTestRun, IDisposable
{
public TestRun()
{
// Configure report options for the ReportingTestFramework
ReportLifecycle.Options = new ReportConfigurationOptions
{
SpecificationsTitle = "My API Specifications",
SeparateSetup = true,
};
// Optional: start any HTTP fakes here
}
public void Dispose()
{
EndRunTime = DateTime.UtcNow;
// Optional: dispose HTTP fakes here
}
}Note: Reports are generated automatically by
ReportingTestFrameworkafter all tests complete — you do not need to callXUnit2ReportGenerator.CreateStandardReportsWithDiagramsinDispose(). If you prefer manual control, see the Alternative: Collection Fixture Approach section below.
Create a collection definition that ties all your diagrammed tests to the TestRun fixture:
using Kronikol.xUnit2;
namespace MyApi.Tests.Component;
[CollectionDefinition(DiagrammedComponentTest.DiagrammedTestCollectionName)]
public class DiagrammedTestCollection : ICollectionFixture<Infrastructure.TestRun> { }Create Infrastructure/BaseFixture.cs. All your test classes will inherit from this:
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Kronikol.xUnit2;
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 XUnit2TestTrackingMessageHandlerOptions
{
CallerName = ServiceUnderTestName,
PortsToServiceNames =
{
{ 80, ServiceUnderTestName },
{ 5001, "Downstream Service A" }
}
});
});
});
}
protected BaseFixture()
{
Client = SFactory!.CreateTestTrackingClient(
new XUnit2TestTrackingMessageHandlerOptions
{
FixedNameForReceivingService = ServiceUnderTestName
});
}
protected override void Dispose(bool disposing)
{
if (disposing) Client?.Dispose();
base.Dispose(disposing);
}
}Key points:
-
DiagrammedComponentTestapplies[Collection("Diagrammed Test Collection")]and[TestTracking]automatically. -
[TestTracking]is aBeforeAfterTestAttributethat sets the current test identity inAsyncLocalbefore each test, and collects scenario metadata. -
XUnit2TestTrackingMessageHandlerOptionsreads the current test identity from theAsyncLocalcontext.
Tests are written as regular xUnit [Fact] or [Theory] methods. Use the [Endpoint] and [HappyPath] attributes to add metadata for the report.
using Kronikol.xUnit2;
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();
}
}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);
}
}dotnet testAfter 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 |
You can customise diagrams within a test using TrackingDiagramOverride:
using Kronikol.xUnit2;
// 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 = trueis set onReportConfigurationOptions, HTTP calls made beforeStartAction()are wrapped in a visual "Setup" partition in the diagram.
┌─────────────────────────────────┐
│ DiagrammedTestCollection │ ← Collection definition (one per assembly)
│ ICollectionFixture<TestRun> │
└─────────────┬───────────────────┘
│ creates once
▼
┌─────────────────────────────────┐
│ TestRun │ ← Sets ReportLifecycle.Options, starts fakes
│ : DiagrammedTestRun │
└─────────────────────────────────┘
│ shared across
▼
┌─────────────────────────────────┐
│ BaseFixture │ ← Creates tracked HttpClient
│ : DiagrammedComponentTest │ [TestTracking] sets AsyncLocal identity
└─────────────┬───────────────────┘
│ inherited by
▼
┌─────────────────────────────────┐
│ Cake_Feature : BaseFixture │ ← Your test class with [Fact] methods
└─────────────────────────────────┘
│ after all tests
▼
┌─────────────────────────────────┐
│ ReportingTestFramework │ ← Custom xUnit framework (via assembly attr)
│ → ReportingTestFrameworkExecutor│ Captures results via message sink
│ → ReportLifecycle.GenerateReports│ Generates HTML/YAML/PlantUML reports
└─────────────────────────────────┘
| Aspect | xUnit v3 | xUnit v2 |
|---|---|---|
| Test identity |
TestContext.Current (built-in) |
AsyncLocal<T> via [TestTracking] attribute |
| Report generation trigger | DiagrammedTestRun.Dispose() |
ReportingTestFramework (custom XunitTestFramework) |
| Result capture | TestContext.Current.TestState.Result |
TestResultCapturingSink (wraps IMessageSink) |
| Trait attributes | ITraitAttribute.GetTraits() |
ITraitAttribute + ITraitDiscoverer
|
| TestContext collection |
ConcurrentQueue<ITestContext> in DiagrammedComponentTest.Dispose()
|
ConcurrentDictionary<string, ScenarioInfo> in [TestTracking].Before()
|
| Package reference | xunit.v3 |
xunit (v2) |
If you prefer not to use the custom test framework, you can generate reports in TestRun.Dispose() instead. Note that with this approach:
- Test results will not be captured automatically (all will show as "Passed")
- Report generation may be truncated if the testhost exits too quickly
public class TestRun : DiagrammedTestRun, IDisposable
{
public void Dispose()
{
EndRunTime = DateTime.UtcNow;
XUnit2ReportGenerator.CreateStandardReportsWithDiagrams(
StartRunTime,
EndRunTime,
new ReportConfigurationOptions
{
SpecificationsTitle = "My API Specifications"
});
}
}For this approach, do not add the [assembly: TestFramework(...)] attribute.
| 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 |
| 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. |
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:
-
In-memory fake APIs —
WebApplicationFactoryinstances that serve canned responses (see Example Project) -
JustEat HttpClient Interception — handler-level interception, chain with
TestTrackingMessageHandler -
WireMock.Net — real HTTP server on a random port, map in
PortsToServiceNames
See Tracking Dependencies#faking-dependencies-getting-proper-http-tracking for detailed examples of each approach.
- Ensure you have
[assembly: TestFramework("Kronikol.xUnit2.ReportingTestFramework", "Kronikol.xUnit2")]in your project. - Ensure
ReportLifecycle.Optionsis set (e.g. inTestRunconstructor). - Alternatively, if using the collection fixture approach, ensure
TestRun.Dispose()callsXUnit2ReportGenerator.CreateStandardReportsWithDiagrams.
- Make sure each test class inherits from
DiagrammedComponentTest(which applies[TestTracking]), or manually apply[TestTracking]to your test classes or assembly. - The
[TestTracking]attribute collects scenario metadata inBefore(). Without it, no scenarios are tracked.
-
AsyncLocalflows throughawaitcalls and throughWebApplicationFactory'sTestServer. If you spawn new threads manually, the value may not propagate — useasync/awaitinstead.
- This happens when not using the
ReportingTestFramework. The custom framework wraps the execution message sink to captureITestFailedandITestSkippedmessages. Without it, results default to "Passed".
If any test has failed, the specifications files will be blank by design. The TestRunReport.html will still be generated.
The steps above assume a greenfield test project. If you're integrating Kronikol into an existing test suite that already has a shared WebApplicationFactory, custom fixtures, a [Collection] definition, and custom DelegatingHandlers, you don't need to restructure — you can add Kronikol incrementally.
You don't need to create a new DiagrammedTestCollection. Add ICollectionFixture<TestRun> to your existing collection definition:
[CollectionDefinition("My Existing Collection")]
public class MyTestCollection
: ICollectionFixture<MyExistingFixture>,
ICollectionFixture<TestRun> // ← add this
{ }If your tests already inherit from a base class and you can't change that inheritance chain, apply [TestTracking] at the assembly level instead:
// GlobalUsings.cs
[assembly: TestTracking]
[assembly: TestFramework(
"Kronikol.xUnit2.ReportingTestFramework",
"Kronikol.xUnit2")]This is functionally equivalent to inheriting from DiagrammedComponentTest — it sets the AsyncLocal test identity before each test and collects scenario metadata.
If your existing setup uses CreateDefaultClient with custom DelegatingHandlers, add TestTrackingMessageHandler as the first handler:
var trackingHandler = new TestTrackingMessageHandler(
new XUnit2TestTrackingMessageHandlerOptions
{
FixedNameForReceivingService = "My API"
});
Client = factory.CreateDefaultClient(trackingHandler, existingHandler1, existingHandler2);Register MessageTracker via TrackMessagesForDiagrams in ConfigureTestServices, then resolve it from DI in your existing fake infrastructure. If your fakes are created before DI is built, read the tracker from the DI-resolved parent at send time rather than capturing it at construction time. See Event Annotations#tips-for-tracking-in-memory-message-brokers for details.
You don't need to wire everything at once. A practical adoption order:
-
HTTP tracking — Add
TrackDependenciesForDiagrams+CreateTestTrackingClientto get sequence diagrams for HTTP traffic -
Cosmos DB — Add
CosmosTrackingMessageHandlerto see database operations in diagrams -
Messaging — Add
MessageTrackerto see Service Bus / event interactions -
Report polish — Add
[Endpoint],[HappyPath],SeparateSetup, CI summary, etc.
Getting Started
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Search Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
Reference