Skip to main content

Shouldly Assertions

Before You Start

  • Install both testing packages:

    dotnet add package Excalibur.Dispatch.Testing
    dotnet add package Excalibur.Dispatch.Testing.Shouldly
    dotnet add package Shouldly
  • Excalibur.Dispatch.Testing.Shouldly is a separate package from Excalibur.Dispatch.Testing. The Shouldly extensions are kept separate so projects that prefer a different assertion library do not take a dependency on Shouldly.

  • Both packages share the Excalibur.Dispatch.Testing namespace, so the extension methods are automatically available once the package is installed.

Overview

Nine extension methods provide fluent assertions for IDispatchedMessageLog, InMemoryTransportSender, and InMemoryTransportReceiver. All methods are defined in DispatchTestingShouldlyExtensions.

// Defined in: Excalibur.Dispatch.Testing.Shouldly/DispatchTestingShouldlyExtensions.cs
// Namespace: Excalibur.Dispatch.Testing

Dispatch Message Assertions

These methods operate on IDispatchedMessageLog (accessible via harness.Dispatched).

ShouldHaveDispatched<T>()

Asserts that at least one message of type T was dispatched:

harness.Dispatched.ShouldHaveDispatched<CreateOrderAction>();

Failure message: "Expected at least one CreateOrderAction to be dispatched, but none were found."

ShouldHaveDispatched<T>(count)

Asserts that exactly count messages of type T were dispatched:

harness.Dispatched.ShouldHaveDispatched<CreateOrderAction>(2);

Failure message: "Expected 2 CreateOrderAction message(s) to be dispatched."

ShouldNotHaveDispatched<T>()

Asserts that no messages of type T were dispatched:

harness.Dispatched.ShouldNotHaveDispatched<CancelOrderAction>();

Failure message: "Expected no CancelOrderAction to be dispatched, but found 1."

ShouldHaveDispatchedCount(count)

Asserts that the total number of dispatched messages (all types) equals count:

harness.Dispatched.ShouldHaveDispatchedCount(3);

Transport Sender Assertions

These methods operate on InMemoryTransportSender.

ShouldHaveSent(count)

Asserts that exactly count messages were sent through the sender:

sender.ShouldHaveSent(2);

ShouldHaveSentTo(destination)

Asserts that the sender is configured for the specified destination:

sender.ShouldHaveSentTo("orders-topic");

This checks the Destination property, not individual message destinations.

ShouldHaveSentMessageMatching(predicate)

Asserts that at least one sent message matches the predicate:

sender.ShouldHaveSentMessageMatching(m => m.CorrelationId == "corr-001");

sender.ShouldHaveSentMessageMatching(m =>
m.ContentType == "application/json" &&
m.Subject == "OrderCreated");

Transport Receiver Assertions

These methods operate on InMemoryTransportReceiver.

ShouldHaveAcknowledged(count)

Asserts that exactly count messages were acknowledged:

receiver.ShouldHaveAcknowledged(1);

ShouldHaveRejected(count)

Asserts that exactly count messages were rejected:

receiver.ShouldHaveRejected(0);

Complete Example

A test combining the harness, transport test double, and Shouldly assertions:

using Excalibur.Dispatch.Abstractions;
using Excalibur.Dispatch.Testing;
using Excalibur.Dispatch.Testing.Transport;
using Excalibur.Dispatch.Transport;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;

public class OrderFlowShould : IAsyncDisposable
{
private readonly DispatchTestHarness _harness;
private readonly InMemoryTransportSender _sender;

public OrderFlowShould()
{
_sender = new InMemoryTransportSender("orders-topic");

_harness = new DispatchTestHarness()
.ConfigureServices(services =>
{
services.AddSingleton<ITransportSender>(_sender);
})
.ConfigureDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(PlaceOrderHandler).Assembly);
});
}

[Fact]
public async Task Place_order_dispatches_and_sends_to_transport()
{
// Arrange
var context = new MessageContextBuilder()
.WithCorrelationId("corr-001")
.Build();

// Act
await _harness.Dispatcher.DispatchAsync(
new PlaceOrderAction("customer-123", 49.99m),
context,
CancellationToken.None);

// Assert with Shouldly extensions
_harness.Dispatched.ShouldHaveDispatched<PlaceOrderAction>();
_harness.Dispatched.ShouldHaveDispatchedCount(1);
_harness.Dispatched.ShouldNotHaveDispatched<CancelOrderAction>();

_sender.ShouldHaveSentTo("orders-topic");
_sender.ShouldHaveSent(1);
_sender.ShouldHaveSentMessageMatching(m => m.CorrelationId == "corr-001");
}

public async ValueTask DisposeAsync()
{
await _harness.DisposeAsync();
await _sender.DisposeAsync();
}
}

Comparison: Shouldly vs Raw Assertions

The Shouldly extensions reduce boilerplate and produce better failure messages:

With Shouldly extensions:

harness.Dispatched.ShouldHaveDispatched<CreateOrderAction>(2);
sender.ShouldHaveSentMessageMatching(m => m.CorrelationId == "corr-001");
receiver.ShouldHaveAcknowledged(1);

Without (raw assertions):

harness.Dispatched.Select<CreateOrderAction>().Count.ShouldBe(2);
sender.SentMessages.ShouldContain(m => m.CorrelationId == "corr-001");
receiver.AcknowledgedMessages.Count.ShouldBe(1);

Both approaches work. The extensions provide domain-specific failure messages (e.g., "Expected at least one CreateOrderAction to be dispatched" instead of "Expected True but was False").

API Reference

MethodTarget TypeDescription
ShouldHaveDispatched<T>()IDispatchedMessageLogAt least one T dispatched
ShouldHaveDispatched<T>(int)IDispatchedMessageLogExactly N of type T dispatched
ShouldNotHaveDispatched<T>()IDispatchedMessageLogZero of type T dispatched
ShouldHaveDispatchedCount(int)IDispatchedMessageLogTotal dispatched count equals N
ShouldHaveSent(int)InMemoryTransportSenderExactly N messages sent
ShouldHaveSentTo(string)InMemoryTransportSenderSender destination matches
ShouldHaveSentMessageMatching(Func)InMemoryTransportSenderAt least one message matches predicate
ShouldHaveAcknowledged(int)InMemoryTransportReceiverExactly N messages acknowledged
ShouldHaveRejected(int)InMemoryTransportReceiverExactly N messages rejected

See Also