ModulusKit.Testing 3.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package ModulusKit.Testing --version 3.1.0
                    
NuGet\Install-Package ModulusKit.Testing -Version 3.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ModulusKit.Testing" Version="3.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ModulusKit.Testing" Version="3.1.0" />
                    
Directory.Packages.props
<PackageReference Include="ModulusKit.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ModulusKit.Testing --version 3.1.0
                    
#r "nuget: ModulusKit.Testing, 3.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ModulusKit.Testing@3.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ModulusKit.Testing&version=3.1.0
                    
Install as a Cake Addin
#tool nuget:?package=ModulusKit.Testing&version=3.1.0
                    
Install as a Cake Tool

Modulus.Testing

Test harness, in-memory test transport, and outbox/inbox assertion helpers for ModulusKit.Messaging — module-level integration testing without hand-rolling a fake transport or querying the outbox tables by hand.

Installation

dotnet add package ModulusKit.Testing

Setup

Wire messaging exactly as you would in production — AddModulusMessaging with Transport.InMemory — then swap the transport for the test-observable one with a single extra call:

using Microsoft.EntityFrameworkCore;
using Modulus.Messaging;
using Modulus.Testing;

var services = new ServiceCollection();
services.AddLogging();
services.AddDbContext<OutboxDbContext>(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString()));
services.AddModulusMessaging(options =>
{
    options.Transport = Transport.InMemory;
    options.Assemblies.Add(typeof(OrderPlacedEvent).Assembly);
});
services.AddModulusTestTransport(); // must come after AddModulusMessaging

AddModulusTestTransport() replaces the registered IMessageTransport singleton with a TestMessageTransport and registers the same instance under its concrete type, so it is resolvable either way. It throws InvalidOperationException if AddModulusMessaging was not called first — there is nothing to replace.

Running the Pipeline: ModulusMessagingTestHarness

AddModulusMessaging registers hosted services (the transport consumer host, the outbox processor) that a real IHost starts and stops for you. ModulusMessagingTestHarness does the same for a test — in registration order on start, reverse order on stop, matching production exactly (the consumer host subscribes before the outbox processor's first dispatch pass; the outbox processor stops before consumers drain in-flight work):

await using var harness = await ModulusMessagingTestHarness.StartAsync(services);

using var scope = harness.Provider.CreateScope();
var messageBus = scope.ServiceProvider.GetRequiredService<IMessageBus>();

await messageBus.Publish(new OrderPlacedEvent(orderId, customerId, total));

await TestWait.WaitForConditionAsync(() => handler.HandledEvents.Count == 1);

harness.Transport resolves the TestMessageTransport swapped in by AddModulusTestTransport() directly, without an extra GetRequiredService call.

TestMessageTransport

Built entirely against the public transport SPI (IMessageTransport, TransportEnvelope, TransportSubscription, MessageDispatchResult) — no internals access to Modulus.Messaging is required or used. It mirrors the channel-per-event-type delivery semantics of the library's internal in-memory transport, including TransportEnvelope.ScheduledEnqueueTimeUtc timer delivery and MessageDispatchResult.Retry redelivery (broker-native retry mode) with an incremented modulus-delivery-attempt header — so a test written against ConsumerRetryMode.Broker behaves the same as it would against a real broker.

On top of that parity, it adds the observability a test actually needs:

  • Published — every envelope passed to PublishAsync, in order, whether or not anything subscribes to it (a snapshot list; safe to enumerate from a concurrent test).
  • DeadLettered — envelopes the consumer pipeline dead-lettered. The production in-memory transport only logs and drops these; the test transport keeps them so a test can assert a poison message actually reached dead-letter status.
  • PublishFailure — set an Exception and every subsequent PublishAsync call throws it, for testing a caller's failure handling.
  • PublishedEventsOf<TEvent>() / DeadLetteredEventsOf<TEvent>() — deserializes the matching envelope bodies back into TEvent with System.Text.Json, so assertions read typed events instead of raw envelopes.
var published = harness.Transport.PublishedEventsOf<OrderPlacedEvent>();
published.ShouldHaveSingleItem().OrderId.ShouldBe(orderId);

var deadLettered = harness.Transport.DeadLetteredEventsOf<OrderPlacedEvent>();

TestWait

Polls a condition instead of sleeping a fixed interval, so a test passes the instant the condition holds and fails with a clear message when it never does:

await TestWait.WaitForConditionAsync(() => handler.HandledEvents.Count == 1);
await TestWait.WaitForConditionAsync(
    async () => await dbContext.OrderReadModels.AnyAsync(o => o.Id == orderId),
    timeout: TimeSpan.FromSeconds(10),
    because: "the projection handler should have upserted the read model");

Outbox and Inbox Query Helpers

OutboxTestQueries and InboxTestQueries are extension methods on IServiceProvider that resolve OutboxDbContext / InboxDbContext from a fresh scope, so tests assert against the tables the same way the CLI's outbox/inbox commands do, without writing that scope-and-query boilerplate by hand:

using Modulus.Testing;

var pending = await harness.Provider.GetPendingOutboxMessagesAsync();
var deadLettered = await harness.Provider.GetDeadLetteredOutboxMessagesAsync(maxAttempts: 5);
await harness.Provider.WaitForOutboxDrainAsync(TimeSpan.FromSeconds(10));

var processed = await harness.Provider.HasHandlerProcessedAsync(
    eventId, typeof(OrderPlacedEventHandler).FullName!);

Inbox reservation tests need SQLite, not the EF Core in-memory provider. IInboxStore's TryReserve/takeover contract depends on the InboxMessageConsumers composite primary key actually being enforced and on ExecuteUpdateAsync semantics that the in-memory provider does not implement. GetInboxMessagesAsync and HasHandlerProcessedAsync work fine against either provider for read-only assertions, but back InboxDbContext with UseSqlite("DataSource=:memory:") (with the connection kept open for the test's lifetime) for any test that exercises reservation, takeover, or release behavior. The outbox has no such requirement — the EF Core in-memory provider is fine for OutboxTestQueries.

Learn More

See the Modulus documentation for the full testing reference, including a walkthrough of the harness against a scaffolded module.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.0 100 7/26/2026
3.1.0 264 7/26/2026