https://github.com/mauroservienti/nservicebus.integrationtesting
An end-to-end testing toolkit for NServiceBus based messaging systems.
https://github.com/mauroservienti/nservicebus.integrationtesting
hacktoberfest testing
Last synced: 4 months ago
JSON representation
An end-to-end testing toolkit for NServiceBus based messaging systems.
- Host: GitHub
- URL: https://github.com/mauroservienti/nservicebus.integrationtesting
- Owner: mauroservienti
- License: apache-2.0
- Created: 2019-07-03T08:57:38.000Z (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2025-02-13T11:42:52.000Z (over 1 year ago)
- Last Synced: 2025-04-14T11:41:43.128Z (about 1 year ago)
- Topics: hacktoberfest, testing
- Language: C#
- Homepage:
- Size: 722 KB
- Stars: 7
- Watchers: 3
- Forks: 2
- Open Issues: 19
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# NServiceBus.IntegrationTesting
NServiceBus.IntegrationTesting enables testing end-to-end business scenarios against real production endpoints, real transports, and real persistence.
> [!IMPORTANT]
> **Disclaimer**: NServiceBus.IntegrationTesting is not affiliated with Particular Software and is not officially supported by Particular Software.
> [!NOTE]
> Version 3 is a major rewrite with a new out-of-process architecture. Pre-release packages are available via the [Feedz.io pre-releases feed](https://f.feedz.io/mauroservienti/pre-releases/nuget/index.json).
---
## Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop/) installed and running — all endpoints and infrastructure run in containers
- .NET 8 or .NET 10 SDK
## Installation
Install the core test-host package into your test project:
```
dotnet add package NServiceBus.IntegrationTesting
```
Add infrastructure extension packages for the transports and persistence your endpoints use:
```
dotnet add package NServiceBus.IntegrationTesting.RabbitMQ # RabbitMQ transport
dotnet add package NServiceBus.IntegrationTesting.PostgreSql # PostgreSQL persistence or transport
dotnet add package NServiceBus.IntegrationTesting.MySql # MySQL persistence
dotnet add package NServiceBus.IntegrationTesting.SqlServer # SQL Server persistence or transport
dotnet add package NServiceBus.IntegrationTesting.MongoDb # MongoDB persistence
dotnet add package NServiceBus.IntegrationTesting.RavenDb # RavenDB persistence
```
Install the agent package into each `*.Testing` companion project, matching the NServiceBus version that endpoint uses:
| NServiceBus version | Agent package | Target framework |
|---|---|---|
| 10 | `NServiceBus.IntegrationTesting.AgentV10` | net10.0 |
| 9 | `NServiceBus.IntegrationTesting.AgentV9` | net8.0 |
| 8 | `NServiceBus.IntegrationTesting.AgentV8` | net6.0 |
```
dotnet add package NServiceBus.IntegrationTesting.AgentV10 # for NServiceBus 10 endpoints
dotnet add package NServiceBus.IntegrationTesting.AgentV9 # for NServiceBus 9 endpoints
dotnet add package NServiceBus.IntegrationTesting.AgentV8 # for NServiceBus 8 endpoints
```
---
## Architecture: out-of-process with Docker containers
Each endpoint runs in its own Docker container. Endpoints can run **different NServiceBus major versions** side-by-side. The test process acts as a gRPC server; each endpoint embeds a lightweight gRPC agent that connects home on startup.
### How it works
```
Test process
└─ TestHostServer (gRPC, dynamic port)
├─[bidirectional streaming]─► SampleEndpoint container (NSB 10 / .NET 10)
│ └─► RabbitMQ container
│ └─► PostgreSQL container
└─[bidirectional streaming]─► AnotherEndpoint container (NSB 9 / .NET 8)
└─► RabbitMQ container
```
### Writing a test
```cs
[TestFixture]
public class WhenSomeMessageIsSent
{
static TestEnvironment _env = null!;
[OneTimeSetUp]
public static async Task SetUp()
{
var srcDir = Path.Combine(FindRepoRoot(), "src");
_env = await new TestEnvironmentBuilder()
.WithDockerfileDirectory(srcDir)
.UseRabbitMQ()
.UsePostgreSql()
.AddEndpoint("SampleEndpoint", "SampleEndpoint.Testing/Dockerfile")
.AddEndpoint("AnotherEndpoint", "AnotherEndpoint.Testing/Dockerfile")
.StartAsync();
}
[OneTimeTearDown]
public static Task TearDown() => _env.DisposeAsync().AsTask();
[Test]
public async Task The_full_chain_should_be_processed()
{
var correlationId = await _env.GetEndpoint("SampleEndpoint")
.ExecuteScenarioAsync("SomeMessage", new Dictionary
{
{ "ID", Guid.NewGuid().ToString() }
});
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var results = await _env.Observe(correlationId, cts.Token)
.HandlerInvoked("SomeMessageHandler")
.HandlerInvoked("AnotherMessageHandler")
.HandlerInvoked("SomeReplyHandler")
.WhenAllAsync();
Assert.Multiple(() =>
{
Assert.That(results.HandlerInvoked("SomeMessageHandler").EndpointName,
Is.EqualTo("SampleEndpoint"));
Assert.That(results.HandlerInvoked("AnotherMessageHandler").EndpointName,
Is.EqualTo("AnotherEndpoint"));
});
}
[Test]
public async Task A_failing_message_is_reported()
{
var correlationId = await _env.GetEndpoint("SampleEndpoint")
.ExecuteScenarioAsync("FailingMessage", new Dictionary
{
{ "ID", Guid.NewGuid().ToString() }
});
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var results = await _env.Observe(correlationId, cts.Token)
.MessageFailed()
.WhenAllAsync();
var failure = results.MessageFailed();
Assert.That(failure.EndpointName, Is.EqualTo("AnotherEndpoint"));
Assert.That(failure.ExceptionMessage, Does.Contain("Intentional failure"));
}
static string FindRepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !dir.GetDirectories(".git").Any())
dir = dir.Parent;
return dir?.FullName
?? throw new InvalidOperationException(
"Cannot locate repository root. Ensure the test runs inside a git repository.");
}
}
```
snippet source | anchor
> [!NOTE]
> The example uses **NUnit** (`[TestFixture]`, `[OneTimeSetUp]`, `Assert.Multiple`). xUnit or MSTest users need to adapt the fixture lifecycle and assertion calls accordingly.
`FindRepoRoot()` is a helper you write once in your test project. It walks up from the test output directory until it finds a marker — `.git`, a `.sln` file, or any other anchor that matches where your Dockerfiles are rooted — giving `TestEnvironmentBuilder` a stable base path regardless of where `dotnet test` is invoked from.
For a full walkthrough and API reference, see the **[documentation](https://github.com/mauroservienti/NServiceBus.IntegrationTesting/blob/master/docs/README.md)** or the **[getting started guide](https://github.com/mauroservienti/NServiceBus.IntegrationTesting/blob/master/docs/getting-started.md)**.
### Key concepts
**`TestEnvironmentBuilder`** — fluent builder that starts a Docker network, infrastructure containers, the gRPC test host, and all endpoint containers. Optionally adds [WireMock.Net](https://github.com/WireMock-Net/WireMock.Net) for HTTP stubbing via `.UseWireMock()`. It automatically injects `NSBUS_TESTING_HOST` into every endpoint container so the agent knows where to connect — endpoints do not configure this themselves.
**Scenarios** — named entry points defined in the `*.Testing` companion project. A scenario runs _inside the endpoint process_, using the real `IMessageSession`, so no cross-process message serialization occurs:
```cs
public class SomeMessageScenario : Scenario
{
public override string Name => "SomeMessage";
public override async Task Execute(IMessageSession session,
Dictionary args, CancellationToken cancellationToken = default)
=> await session.Send(new SomeMessage { Id = Guid.Parse(args["ID"]) });
}
```
snippet source | anchor
**`ObserveContext`** — fluent API for waiting on events correlated by scenario invocation. Register all conditions before calling `.WhenAllAsync()` — the same method names serve double duty: called on the builder they register a condition; called on the returned `ObserveResults` they retrieve the matched event. If the cancellation token fires before all conditions are met, `WhenAllAsync()` throws `OperationCanceledException`. Each condition type supports no-arg, single-event predicate, and list predicate overloads.
| Condition | Key result properties |
|---|---|
| `.HandlerInvoked(name)` | `.EndpointName` |
| `.SagaInvoked(name)` | `.EndpointName`, `.SagaIsNew`, `.SagaIsCompleted` |
| `.MessageDispatched(name)` | `.EndpointName`, `.Intent` (e.g. `"Publish"`, `"Send"`) |
| `.MessageFailed()` | `.EndpointName`, `.ExceptionMessage` |
**Production / testing separation** — production endpoints have zero testing dependencies. Each endpoint has a companion `*.Testing` project that wraps the production config with `IntegrationTestingBootstrap` and registers scenarios.
### Why `*.Testing` companion projects?
The production endpoint (`SampleEndpoint/`) has no reference to any testing library. It stays clean and deployable as-is.
The `*.Testing` companion project exists solely for integration tests. It:
1. **Wraps the production config** — calls the same `Config.Create()` factory used in
production, then overlays test-specific settings (e.g. shorter retry counts so failing
messages reach the error queue quickly instead of spending minutes in retry loops).
2. **Registers scenarios** — named entry points that run _inside_ the endpoint process
using the real `IMessageSession`. Because scenarios execute in-process, messages are
created and sent natively — no cross-process serialization, no test-only message
constructors, no leaking of test concerns into production types.
3. **Provides the Dockerfile** — builds a container image from the companion project, not
from the production project. Only the testing image carries the agent and scenario
registrations. A minimal `Dockerfile` (build context: `src/`) looks like:
```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY YourMessages/YourMessages.csproj YourMessages/
COPY YourEndpoint/YourEndpoint.csproj YourEndpoint/
COPY YourEndpoint.Testing/YourEndpoint.Testing.csproj YourEndpoint.Testing/
RUN dotnet restore YourEndpoint.Testing/YourEndpoint.Testing.csproj
COPY YourMessages/ YourMessages/
COPY YourEndpoint/ YourEndpoint/
COPY YourEndpoint.Testing/ YourEndpoint.Testing/
RUN dotnet publish YourEndpoint.Testing/YourEndpoint.Testing.csproj -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/runtime:10.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "YourEndpoint.Testing.dll"]
```
```text
SampleEndpoint/ ← production code, zero test dependencies
SampleEndpoint.Testing/ ← wraps production config, adds agent + scenarios
Program.cs ← IntegrationTestingBootstrap.RunAsync(...)
SomeMessageScenario.cs ← implements Scenario base class
Dockerfile ← builds the testable container image
SampleEndpoint.Tests/ ← NUnit test project, references Testing only for
WhenSomeMessageIsSent.cs compile-time validation (ReferenceOutputAssembly=false)
```
The test project references the `*.Testing` project with `ReferenceOutputAssembly="false"`. This gives compile-time validation that the `*.Testing` project builds, while at test runtime the container image is built from its Dockerfile — there is no in-process loading of the endpoint assembly.
## NuGet packages
| Package | Purpose |
|---|---|
| `NServiceBus.IntegrationTesting` | Test host, `TestEnvironmentBuilder`, observe API |
| `NServiceBus.IntegrationTesting.RabbitMQ` | RabbitMQ transport container |
| `NServiceBus.IntegrationTesting.PostgreSql` | PostgreSQL persistence container |
| `NServiceBus.IntegrationTesting.MySql` | MySQL persistence container |
| `NServiceBus.IntegrationTesting.SqlServer` | SQL Server persistence container |
| `NServiceBus.IntegrationTesting.MongoDb` | MongoDB persistence container |
| `NServiceBus.IntegrationTesting.RavenDb` | RavenDB persistence container |
| `NServiceBus.IntegrationTesting.AgentV10` | Agent for NServiceBus 10 (net10.0) |
| `NServiceBus.IntegrationTesting.AgentV9` | Agent for NServiceBus 9 (net8.0) |
| `NServiceBus.IntegrationTesting.AgentV8` | Agent for NServiceBus 8 (net6.0) |
- [NuGet stable releases](https://www.nuget.org/packages/NServiceBus.IntegrationTesting)
- [Feedz.io pre-releases feed](https://f.feedz.io/mauroservienti/pre-releases/nuget/index.json)
---
Icon [test](https://thenounproject.com/search/?q=test&i=2829166) by Andrei Yushchenko from the Noun Project