本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
测试 AMQP 服务
测试用 TestApp 运行服务自己的应用,连接的是进程内 Broker 或者正在运行的 Broker。
被测服务负责银行卡扣款。
付款到达队列 card-payments,处理器在 card-receipts 上以一张收据作为回复。
没有银行卡的付款被丢弃。
超出银行即时限额的付款会被暂缓:处理器用 retry_after 要求一分钟后再投递它。
经过三次投递后,付款进入死信地址 card-payments.review。
应用由 service(broker) 构建,所以测试可以选择 Broker 的 URL,而 main 使用生产环境的 URL。
测试需要在 [dev-dependencies] 中启用 ruststream-amqp 的 testing feature。
付款及其收据
- 宏
- 手写
#[tokio::test]
async fn a_payment_gets_a_receipt() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(AmqpBroker::new(URL))).await?;
a_charged_payment_gets_its_receipt(tb, 1).await
}
async fn a_charged_payment_gets_its_receipt(
tb: TestApp<()>,
id: u64,
) -> Result<(), Box<dyn Error>> {
tb.broker::<AmqpBroker>()
.message(&payment(id, 2_500))
.publish()
.await?;
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.broker::<AmqpBroker>()
.published::<Receipt>("card-receipts")
.assert_called_once()
.with(&Receipt {
payment: id,
amount_cents: 2_500,
});
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn a_payment_gets_a_receipt() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(AmqpBroker::new(URL))).await?;
a_charged_payment_gets_its_receipt(tb, 1).await
}
async fn a_charged_payment_gets_its_receipt(
tb: TestApp<()>,
id: u64,
) -> Result<(), Box<dyn Error>> {
tb.broker::<AmqpBroker>()
.message(&payment(id, 2_500))
.publish()
.await?;
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.broker::<AmqpBroker>()
.published::<Receipt>("card-receipts")
.assert_called_once()
.with(&Receipt {
payment: id,
amount_cents: 2_500,
});
tb.shutdown().await?;
Ok(())
}
TestApp::start 把 AmqpBroker 连接到进程内 Broker。
测试从不连接 Broker 的 URL。
测试按生产类型访问 Broker:tb.broker::<AmqpBroker>()。
publish 在处理器及其回复都完成结算后才返回。
所以接下来的几行断言的是已经完成的状态:订阅者如何结算,以及什么到达了 card-receipts。
测试主体把已启动的应用和付款的 id 作为参数接收,所以下面的线上测试也运行它。
没有银行卡的付款
- 宏
- 手写
#[tokio::test]
async fn a_payment_without_a_card_is_dropped() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(AmqpBroker::new(URL))).await?;
tb.broker::<AmqpBroker>()
.message(&Payment {
id: 2,
card: String::new(),
amount_cents: 2_500,
})
.publish()
.await?;
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.broker::<AmqpBroker>()
.published::<Receipt>("card-receipts")
.assert_not_called();
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn a_payment_without_a_card_is_dropped() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(AmqpBroker::new(URL))).await?;
tb.broker::<AmqpBroker>()
.message(&Payment {
id: 2,
card: String::new(),
amount_cents: 2_500,
})
.publish()
.await?;
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.broker::<AmqpBroker>()
.published::<Receipt>("card-receipts")
.assert_not_called();
tb.shutdown().await?;
Ok(())
}
settled 检查处理器返回的结果。
assert_not_called 检查没有发布任何收据。
不必等待的延迟
- 宏
- 手写
#[tokio::test(start_paused = true)]
async fn a_payment_the_bank_keeps_holding_goes_to_review() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(AmqpBroker::new(URL))).await?;
let held = payment(3, 250_000);
tb.broker::<AmqpBroker>().message(&held).publish().await?;
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::retry_after(HOLD));
// The delay is a copy of the payment, published back to its queue after `HOLD`.
tb.advance(HOLD).await?;
tb.broker::<AmqpBroker>()
.published::<Payment>("card-payments")
.assert_called(2)
.with(&held)
.with_header(RETRY_COUNT_HEADER, "1");
// The third delivery is the last one: it goes to the review address as it arrived.
tb.advance(HOLD).await?;
assert_eq!(
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.outcomes(),
[Outcome::Nack, Outcome::Nack, Outcome::Nack],
);
tb.broker::<AmqpBroker>()
.published::<Payment>("card-payments.review")
.assert_called_once()
.with(&held);
tb.shutdown().await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn a_payment_the_bank_keeps_holding_goes_to_review() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(AmqpBroker::new(URL))).await?;
let held = payment(3, 250_000);
tb.broker::<AmqpBroker>().message(&held).publish().await?;
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::retry_after(HOLD));
// The delay is a copy of the payment, published back to its queue after `HOLD`.
tb.advance(HOLD).await?;
tb.broker::<AmqpBroker>()
.published::<Payment>("card-payments")
.assert_called(2)
.with(&held)
.with_header(RETRY_COUNT_HEADER, "1");
// The third delivery is the last one: it goes to the review address as it arrived.
tb.advance(HOLD).await?;
assert_eq!(
tb.broker::<AmqpBroker>()
.subscriber("card-payments")
.outcomes(),
[Outcome::Nack, Outcome::Nack, Outcome::Nack],
);
tb.broker::<AmqpBroker>()
.published::<Payment>("card-payments.review")
.assert_called_once()
.with(&held);
tb.shutdown().await?;
Ok(())
}
测试在暂停的时钟下启动,advance 把时钟向前拨。
一分钟的 HOLD 不花任何时间。
AMQP 1.0 没有延迟重新投递,所以延迟结束时,retry_after 把付款的一份副本发布到它的队列。
副本在消息头里带着重试次数,published 把它显示在 card-payments 上,与测试自己发布的消息并列。
第三次投递是挂载点允许的最后一次,所以付款按到达时的原样进入死信地址 card-payments.review。
每次被暂缓的投递都记录为 Outcome::Nack。
进程内 Broker 模拟了什么
进程内 Broker 像 connect 一样读取 Broker 的 URL。
客户端无法打开的 URL 会让测试启动失败;在这里,amqps:// 同样需要 rustls 或 native-tls feature。
每次发布都会转换成 AMQP 消息再转换回来,所以处理器读到的消息头与线上投递带的相同。
它模拟 Broker 的路由:
queue地址的各个消费者相互竞争,每次投递只交给一个;每个topic订阅都收到一份副本。raw地址按原样投递给一个消费者。- 队列订阅会创建它的队列,没有消费者附加期间到达的消息由队列保留。
retry在delivery-count中计入这次尝试,未结算的投递在其订阅结束时回到队列。- 至多一次订阅的投递已经结算,
ack返回AckError::Unsupported。 - 事务在提交前什么也不发布,无人应答的请求会超时。
链路信用、Broker 的死信策略和投递上限、持久性,以及 Broker 为发布创建的地址,都需要正在运行的 Broker。 在进程内,发往尚无队列订阅打开的地址的消息会被丢弃。
在 Broker 上运行同一个测试
- 宏
- 手写
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_payment_gets_a_receipt_from_a_broker() -> Result<(), Box<dyn Error>> {
let Some(url) = live::url("AMQP_TEST_URL") else {
return Ok(());
};
let tb = TestApp::start_live(service(AmqpBroker::new(url))).await?;
a_charged_payment_gets_its_receipt(tb, u64::from(process::id())).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_payment_gets_a_receipt_from_a_broker() -> Result<(), Box<dyn Error>> {
let Some(url) = live::url("AMQP_TEST_URL") else {
return Ok(());
};
let tb = TestApp::start_live(service(AmqpBroker::new(url))).await?;
a_charged_payment_gets_its_receipt(tb, u64::from(process::id())).await
}
TestApp::start_live 把同一个应用连接到正在运行的 Broker。
测试用 live::url(示例自己的辅助函数)从 AMQP_TEST_URL 读取 URL,未设置时跳过。
付款的 id 是进程 id,因此可以在 Broker 上区分每次运行的消息。
断言与进程内的完全相同。
cargo test
AMQP_TEST_URL=amqp://artemis:artemis@localhost:5672 cargo test -- --test-threads=1