-
Notifications
You must be signed in to change notification settings - Fork 1
Multi Host Test Architectures
Many real-world microservices have a dual-host architecture:
-
API host —
WebApplicationFactory<T>serving HTTP endpoints -
Function host — Azure Functions
FunctionTestServer<T>processing async triggers (Service Bus, Change Feed, etc.)
Both hosts share the same databases, messaging infrastructure, and caches. This page covers the patterns for making them work together with Kronikol.
Single-host event-driven? If your SUT is a single application that both consumes messages and calls dependencies (no separate Function host), see Event-Driven Architecture Testing for a simpler setup pattern.
When using in-memory messaging (e.g. AddInMemoryMessaging()), both hosts must share the same InMemoryMessaging and IServiceBusMessageSender instances. Otherwise, messages published by the API host are invisible to the Function host's trigger framework.
If the messaging framework uses AddSingleton (not TryAddSingleton), it overwrites previously registered instances. Register your shared instances after the framework's registration:
// ❌ WRONG — framework's AddInMemoryMessaging overwrites these
serviceCollection.AddSingleton(sharedInMemoryMessaging);
serviceCollection.AddSingleton<IServiceBusMessageSender>(sharedSender);
serviceCollection.AddInMemoryMessaging(options); // overwrites above
// ✅ CORRECT — register after to overwrite the framework's registrations
serviceCollection.AddInMemoryMessaging(options);
serviceCollection.AddSingleton(sharedInMemoryMessaging); // overwrites
serviceCollection.AddSingleton<IServiceBusMessageSender>(sharedSender); // overwritesIf messages aren't flowing between hosts, verify the instances are shared:
var webMessaging = WebFactory.Services.GetRequiredService<InMemoryMessaging>();
var funcMessaging = Functions.ServiceProvider.GetRequiredService<InMemoryMessaging>();
Assert.True(ReferenceEquals(webMessaging, funcMessaging), "Hosts must share the same InMemoryMessaging");TrackMessagesForDiagrams() registers a MessageTracker in one DI container. To track messages sent from the other host, bridge the tracker manually:
public async ValueTask InitializeAsync()
{
// 1. Create the API host (registers MessageTracker in its DI)
WebFactory = new CustomWebApplicationFactory<Startup>(...);
// 2. Create the Function host (separate DI, no MessageTracker)
Functions = new FunctionFixture(...);
await Functions.InitializeAsync();
// 3. Bridge the tracker to the Function's sender
var messageTracker = WebFactory.Services.GetService<MessageTracker>();
if (messageTracker is not null)
{
sender.AfterPublish += (_, args) =>
{
var topicOrQueue = args.QueueOrTopic ?? "unknown";
messageTracker.TrackSendEvent(
protocol: "Publish (Service Bus)",
destinationName: "Service Bus",
destinationUri: new Uri($"servicebus://service-bus/{topicOrQueue}"),
payload: args.Message);
};
}
}Track once, share everywhere:
Test Process
├── API Host (WebApplicationFactory)
│ ├── DI Container 1
│ │ ├── MessageTracker ← registered here
│ │ ├── TrackingDistributedCache ← created here
│ │ ├── CosmosTrackingMessageHandler ← via InMemoryCosmos
│ │ └── TestTrackingMessageHandler ← for outbound HTTP
│
├── Function Host (FunctionTestServer)
│ ├── DI Container 2
│ │ ├── ServiceBusMessageSender ← needs tracker from Container 1
│ │ ├── IDistributedCache ← needs shared instance from Container 1
│ │ └── CosmosDB ← shared CosmosDbFixture
│
└── Shared Fixtures (xUnit Collection)
├── CosmosDbFixture (singleton across both hosts)
└── InMemoryMessaging (bridges message flow)
Key rules:
- Create the API host first — its tracked instances are passed to the Function host
- The
CosmosDbFixtureis created before both hosts and shared via xUnit collection fixtures - Use
TestIdentityScope.Begin()to propagate test identity into the Function host's background processing
If your FunctionTestServer exposes only a plain HttpClient (without access to the underlying TestServer), requests to the Function host bypass Kronikol. Options:
-
Request the FunctionTestServer package to expose its
TestServerorHttpMessageHandler— this is the proper fix -
Use
RequestResponseLogger.LogPair()to manually log Function interactions — see Tracking Custom Dependencies - Wrap with reflection (fragile, last resort):
var hostField = typeof(FunctionTestServer<Startup>)
.GetField("_host", BindingFlags.NonPublic | BindingFlags.Instance);
var host = (IHost)hostField!.GetValue(functionServer)!;
var testServer = host.Services.GetRequiredService<TestServer>();
var trackedClient = testServer.CreateTestTrackingClient(options);All tracking (Cosmos, HTTP, Service Bus) must use the same fetcher delegate so they all resolve the same test identity:
public class TestFixture : IAsyncLifetime
{
public ActiveTestTracker TestTracker { get; } = new();
public async ValueTask InitializeAsync()
{
var fetcher = TestTracker.Fetcher;
// All tracking uses the same fetcher
CosmosDbFixture = new CosmosDbFixture(fetcher);
WebFactory = new CustomWebApplicationFactory(fetcher);
FunctionFixture = new FunctionFixture(fetcher);
}
}For background thread correlation (change feed processors, message handlers), see Background Thread Correlation.
When using CosmosTrackingMessageHandler with a multi-host setup, IHttpContextAccessor is typically not available at Cosmos client construction time. The handler captures the accessor reference from options at construction time.
Use the LazyHttpContextAccessor pattern — see Background Thread Correlation#LazyHttpContextAccessor Pattern — to provide a non-null wrapper at construction time that delegates to the real accessor once WebApplicationFactory is available:
// 1. Create lazy accessor before building Cosmos client
var lazyAccessor = new LazyHttpContextAccessor();
cosmosTrackingOptions.HttpContextAccessor = lazyAccessor;
// 2. Build Cosmos client with tracking handler
var cosmos = InMemoryCosmos.Builder()
.WrapHandler(h => new CosmosTrackingMessageHandler(cosmosTrackingOptions, h))
.Build();
// 3. After WebApplicationFactory.CreateClient(), wire the real accessor
lazyAccessor.SetInner(factory.Services.GetRequiredService<IHttpContextAccessor>());1. Create CosmosDbFixture (shared singleton, tracking handler wraps InMemory handler)
2. Create API host (WebApplicationFactory) — owns MessageTracker, IHttpContextAccessor
3. Create tracked test client (CreateTestTrackingClient)
4. Wire CosmosTrackingMessageHandler.HttpContextAccessor (LazyHttpContextAccessor.SetInner)
5. Create Function host — inject shared Cosmos, shared messaging, shared sender
6. Initialize Function host
7. Bridge MessageTracker to Function host's message sender (AfterPublish events)
8. Wire Service Bus tracking handler
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