An API call publishes an event
Paying an invoice should publish invoice.paid. The test pays over the API and then waits for that event — not with a sleep, but with a predicate and a timeout. This is what a single API assertion cannot prove: that the write side and the broker actually meet.
The same journey runs in the demo — MessagingJourney.cs (test) and Setup.cs (host). The full API surface is in Messaging.
Compose
protected override void Configure(IProtoHostBuilder builder) =>
builder
// One broker for the run, its address handed to the tests and to the application.
.AddInfrastructure(
RabbitMqBroker.Container(),
RabbitMqOptions.ConnectionStringSetting,
"Messaging:RabbitMq:ConnectionString")
.AddApplication("Api", app => app
.AddAspNetCoreServer<Program>()
.AddRest(rest => rest.AddClient("Api")))
// Last, so the application has declared its exchanges before the test's taps bind to them.
.AddMessaging(messaging => messaging.UseRabbitMq());
Declare the destinations the suite awaits, so the adapter binds its tap before the test acts:
{
"ProtoTest": {
"Messaging": {
"Destinations": [ "invoice.paid" ]
}
}
}
The test
[Application("Api")]
public sealed class InvoiceTests
{
[ProtoTest]
public async Task Paying_an_invoice_publishes_invoice_paid()
{
using var issued = await Proto.Context.Rest()
.Body(new { customer = $"customer-{Proto.Context.TestId}", amount = 120 })
.PostAsync("/api/invoices");
var invoice = issued
.Should.HaveHttpStatus(HttpStatusCode.Created)
.ReadAsJson<InvoiceResponse>()!;
using var paid = await Proto.Context.Rest()
.Body(new { method = "visa" })
.PostAsync("/api/invoices/{invoiceId}/pay", new { invoiceId = invoice.Id });
paid.Should.HaveHttpStatus(HttpStatusCode.OK);
var message = await Proto.Context.Messaging().AwaitAsync(
"invoice.paid",
candidate => candidate.Payload?.Contains($"\"id\":{invoice.Id}") == true,
TimeSpan.FromSeconds(15));
Assert.That(message.ContentType, Is.EqualTo("application/json"));
}
private sealed record InvoiceResponse(int Id);
}
What it proves
The 200 says the payment was accepted; the awaited message says the application told the world. Both operations land in the same test's trace: the REST request (with its response) and the messaging.await that matched, in order.
Limits
- The await proves arrival, not delivery guarantees. One matching message on this test's tap says nothing about duplicates, ordering or broker durability.
- Match on something this test owns. Parallel tests pay invoices too; a predicate on the invoice id awaits this test's event, not the first
invoice.paidthat happens to arrive. - Declare before you await. An undeclared destination is bound when
AwaitAsyncis called, and an event published before that is missed. See Destinations. - Where there is no broker, skip.
[RequiresCapability(ProtoCapabilityKinds.Broker)]skips the test in an environment without one, instead of passing against the in-memory double. See Skip conditions.