Skip to main content

Testing the service with TestApp

TestApp runs the shop's own app in process: a test publishes an order and checks what each subscriber did and what each topic received.

A test starts the app that main runs, unchanged, with TestApp::start. The broker connects in process, and every subscription runs on the test's runtime. A publish from the test returns once everything it set off has settled, so the next line can check the result. Assertions read two records: what each subscriber received and how it settled, and what was published to each topic. On a paused clock, advance moves time forward, so a delay of a second or an hour costs nothing. Testing needs the crate's testing feature, usually as a dev-dependency.

An order from start to finish​

The test places an order with its request headers and follows it to the customer. The payment request travels as CBOR, so the assertion decodes it with with_codec. accept and dispatch each send a notice, and send acks both.

main.rs
#[tokio::test]
async fn a_placed_order_is_charged_shipped_and_announced() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.message(&order(7, mugs(12)))
.with_headers(&meta("req-7"))
.publish()
.await?;

let broker = tb.broker::<MemoryBroker>();
broker
.published::<PaymentRequest>("payments.requested")
.assert_called_once()
.with_codec(
&CborCodec,
&PaymentRequest {
order: 7,
customer: "ada@example.com".to_owned(),
lines: mugs(12),
amount_cents: 14_400,
request_id: "req-7".to_owned(),
},
);
broker
.published::<Notice>("notices")
.assert_called(2)
.with(&Notice {
customer: "ada@example.com".to_owned(),
text: "Order 7 is on its way: TRK-7-2.".to_owned(),
});
broker
.subscriber("notices")
.assert_called(2)
.settled(HandlerOutcome::ack());
Ok(())
}

An order that goes no further​

An order with no lines stops at accept: nothing is charged and no customer is told.

main.rs
#[tokio::test]
async fn an_order_with_no_lines_goes_no_further() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.message(&order(8, Vec::new()))
.with_headers(&meta("req-8"))
.publish()
.await?;

let broker = tb.broker::<MemoryBroker>();
broker
.published::<PaymentRequest>("payments.requested")
.assert_not_called();
broker.published::<Notice>("notices").assert_not_called();
Ok(())
}

A delay, without waiting for it​

A provider that takes one charge a second refuses the second request with Busy. with_state starts the app with that provider instead of the one on_startup opens. test_broker in service.rs returns the broker the tests build the shop on, so they name it in one place. The clock starts paused, and advance(BACKOFF) brings the refused request back.

payments.rs
#[tokio::test(start_paused = true)]
async fn a_busy_provider_is_asked_again_after_the_backoff() -> Result<(), Box<dyn Error>> {
// A provider that takes one charge a second.
let state = Shop {
gateway: Arc::new(Gateway::new(100_000, 1)),
..Shop::open().await?
};
let tb = TestApp::with_state(shop(test_broker()), |_| state).await?;
let broker = tb.broker::<MemoryBroker>();
for order in [1, 2] {
broker
.message(&request(order, 500))
.with_codec(CborCodec)
.publish()
.await?;
}
broker
.subscriber("payments.requested")
.assert_called(2)
.settled(HandlerOutcome::retry_after(BACKOFF));

tb.advance(BACKOFF).await?;
broker
.subscriber("payments.requested")
.assert_called(3)
.settled(HandlerOutcome::ack());
broker
.published::<Paid>("payments.settled")
.assert_called(2);
Ok(())
}

The dead-letter topic​

A provider that never answers makes every delivery fail. The fifth delivery goes to payments.dead, as it arrived.

payments.rs
#[tokio::test]
async fn a_silent_provider_sends_the_request_to_the_dead_letter_topic()
-> Result<(), Box<dyn Error>> {
let state = Shop::open().await?;
state.gateway.go_offline();
let tb = TestApp::with_state(shop(test_broker()), |_| state).await?;
let broker = tb.broker::<MemoryBroker>();
broker
.message(&request(3, 500))
.with_codec(CborCodec)
.publish()
.await?;

broker.subscriber("payments.requested").assert_called(5);
broker
.published::<PaymentRequest>("payments.dead")
.assert_called_once()
.with_codec(&CborCodec, &request(3, 500));
Ok(())
}

Run the tests of both forms:

cargo test --example guide --features testing,macros,memory,json,cbor,asyncapi
cargo test --example manual_guide --no-default-features --features testing,memory,json,cbor,asyncapi

The tests in both directories are the same, and they pass against both forms of the shop.