本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
测试 Pulsar 服务
测试用 TestApp 运行服务自己的应用,连接的是进程内 Pulsar 或者运行中的服务器。
被测服务负责银行卡扣款。
付款到达 card-payments,它的分区键就是要扣款的账户。
处理器在 card-receipts 上以一张收据作为回复。
没有键的付款被丢弃。
超出银行即时限额的付款会被暂缓:处理器用 retry_after 要求一分钟后再投递它。
经过三次投递后,付款进入死信主题 card-payments-review。
应用由 service(broker) 构建,所以测试可以选择 Broker,而 main 使用生产环境的 Broker。
测试需要在 [dev-dependencies] 中启用 ruststream-pulsar 的 testing feature。
带键的付款
- 宏
- 手写
#[tokio::test]
async fn a_keyed_payment_gets_a_receipt() -> Result<(), Box<dyn Error>> {
let broker = PulsarBroker::new("pulsar://localhost:6650").default_subscription("payments");
let tb = TestApp::start(service(broker)).await?;
a_receipt_names_the_account_of_the_key(tb).await
}
async fn a_receipt_names_the_account_of_the_key(tb: TestApp<()>) -> Result<(), Box<dyn Error>> {
let mut key = HeaderMap::new();
key.insert(PARTITION_KEY_HEADER, "acct-7");
tb.broker::<PulsarBroker>()
.message(&Payment {
id: 1,
amount_cents: 2_500,
})
.with_headers(key)
.publish()
.await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.broker::<PulsarBroker>()
.published::<Receipt>("card-receipts")
.assert_called_once()
.with(&Receipt {
payment: 1,
account: "acct-7".to_owned(),
});
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn a_keyed_payment_gets_a_receipt() -> Result<(), Box<dyn Error>> {
let broker = PulsarBroker::new("pulsar://localhost:6650").default_subscription("payments");
let tb = TestApp::start(service(broker)).await?;
a_receipt_names_the_account_of_the_key(tb).await
}
async fn a_receipt_names_the_account_of_the_key(tb: TestApp<()>) -> Result<(), Box<dyn Error>> {
let mut key = HeaderMap::new();
key.insert(PARTITION_KEY_HEADER, "acct-7");
tb.broker::<PulsarBroker>()
.message(&Payment {
id: 1,
amount_cents: 2_500,
})
.with_headers(key)
.publish()
.await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.broker::<PulsarBroker>()
.published::<Receipt>("card-receipts")
.assert_called_once()
.with(&Receipt {
payment: 1,
account: "acct-7".to_owned(),
});
tb.shutdown().await?;
Ok(())
}
TestApp::start 把 PulsarBroker 连接到进程内 Pulsar。
服务 URL 按客户端的解析方式校验,但从不去连接它。
测试按生产类型访问 Broker:tb.broker::<PulsarBroker>()。
分区键以 PARTITION_KEY_HEADER 消息头传递,所以测试用 with_headers 设置它。
Broker 把这个消息头变成消息的键,处理器再从同一个消息头把它读回来。
publish 在处理器及其回复都完成结算后才返回。
所以接下来的几行断言的是已经完成的状态:订阅者如何结算,以及什么到达了 card-receipts。
测试主体把已启动的应用作为参数接收,所以下面在服务器上运行的测试也执行它。
没有键的付款
- 宏
- 手写
#[tokio::test]
async fn a_payment_without_an_account_is_dropped() -> Result<(), Box<dyn Error>> {
let broker = PulsarBroker::new("pulsar://localhost:6650").default_subscription("payments");
let tb = TestApp::start(service(broker)).await?;
tb.broker::<PulsarBroker>()
.message(&Payment {
id: 2,
amount_cents: 2_500,
})
.publish()
.await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.broker::<PulsarBroker>()
.published::<Receipt>("card-receipts")
.assert_not_called();
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn a_payment_without_an_account_is_dropped() -> Result<(), Box<dyn Error>> {
let broker = PulsarBroker::new("pulsar://localhost:6650").default_subscription("payments");
let tb = TestApp::start(service(broker)).await?;
tb.broker::<PulsarBroker>()
.message(&Payment {
id: 2,
amount_cents: 2_500,
})
.publish()
.await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.broker::<PulsarBroker>()
.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 broker = PulsarBroker::new("pulsar://localhost:6650").default_subscription("payments");
let tb = TestApp::start(service(broker)).await?;
let held = Payment {
id: 3,
amount_cents: 250_000,
};
let mut key = HeaderMap::new();
key.insert(PARTITION_KEY_HEADER, "acct-7");
tb.broker::<PulsarBroker>()
.message(&held)
.with_headers(key)
.publish()
.await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::retry_after(HOLD));
// Each delivery waits `HOLD`, then the broker hands the same message over again.
tb.advance(HOLD).await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called(2);
// The third delivery is the last one the mount site allows: after its wait, the client
// moves the payment to the review topic.
tb.advance(HOLD).await?;
tb.advance(HOLD).await?;
assert_eq!(
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.outcomes(),
[Outcome::Nack, Outcome::Nack, Outcome::Nack],
);
tb.broker::<PulsarBroker>()
.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 broker = PulsarBroker::new("pulsar://localhost:6650").default_subscription("payments");
let tb = TestApp::start(service(broker)).await?;
let held = Payment {
id: 3,
amount_cents: 250_000,
};
let mut key = HeaderMap::new();
key.insert(PARTITION_KEY_HEADER, "acct-7");
tb.broker::<PulsarBroker>()
.message(&held)
.with_headers(key)
.publish()
.await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called_once()
.settled(HandlerOutcome::retry_after(HOLD));
// Each delivery waits `HOLD`, then the broker hands the same message over again.
tb.advance(HOLD).await?;
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.assert_called(2);
// The third delivery is the last one the mount site allows: after its wait, the client
// moves the payment to the review topic.
tb.advance(HOLD).await?;
tb.advance(HOLD).await?;
assert_eq!(
tb.broker::<PulsarBroker>()
.subscriber("card-payments")
.outcomes(),
[Outcome::Nack, Outcome::Nack, Outcome::Nack],
);
tb.broker::<PulsarBroker>()
.published::<Payment>("card-payments-review")
.assert_called_once()
.with(&held);
tb.shutdown().await?;
Ok(())
}
测试在暂停的时钟上启动,advance 让时钟前进。
一分钟的 HOLD 不花费任何时间。
retry_after 在延迟期间把付款扣留在服务中,然后否定确认它,所以 Broker 再次投递同一条消息。
重试不发布任何东西:card-payments 上唯一的消息就是测试自己的付款。
第三次投递是挂载点允许的最后一次,所以在它的等待结束后,这笔付款按到达时的样子进入 card-payments-review。
每次被暂缓的投递都记录为 Outcome::Nack。
进程内 Pulsar 模拟了什么
进程内 Pulsar 读取服务器会得到的配置:Broker 的设置、每个描述符,以及每个注册的重试声明。
服务器拒绝的东西,它同样拒绝,并返回真实 Broker 会返回的错误:客户端无法解析的服务 URL、不是主题名称的目的地、Exclusive 订阅的第二个消费者,以及关闭之后仍被使用的句柄。
它模拟的是 Pulsar,而不是一个队列:
- 主题就是它的完整名称,所以
orders和persistent://public/default/orders是同一个主题。 - 每个订阅收到其主题的每条消息各一次,由它的类型选出消费者。
- 被否定确认的消息回到它的订阅,死信策略像客户端那样统计它的投递次数。
- 每个主题保存自己的日志,所以定位会重放它;非持久主题不重放任何东西。
- 模式订阅从客户端的下一次列出开始读取新主题,即暂停的时钟上 30 秒之后。
- 消息头以文本属性传递,超过
max_message_size的载荷会让发布失败。
服务器让第二个 Exclusive 消费者一直等待,而进程内 Pulsar 立即拒绝它,所以这样的测试会失败,而不是挂起。
确认超时之后的重新投递、分区主题、KeyShared 的键范围,以及订阅没有消费者时发布的消息,都需要服务器。
在服务器上运行同一个测试
- 宏
- 手写
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_keyed_payment_gets_a_receipt_on_a_server() -> Result<(), Box<dyn Error>> {
let Some(url) = stand() else {
return Ok(());
};
let subscription = format!("payments-{}", process::id());
let broker = PulsarBroker::new(url).default_subscription(subscription);
let tb = TestApp::start_live(service(broker)).await?;
a_receipt_names_the_account_of_the_key(tb).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_keyed_payment_gets_a_receipt_on_a_server() -> Result<(), Box<dyn Error>> {
let Some(url) = stand() else {
return Ok(());
};
let subscription = format!("payments-{}", process::id());
let broker = PulsarBroker::new(url).default_subscription(subscription);
let tb = TestApp::start_live(service(broker)).await?;
a_receipt_names_the_account_of_the_key(tb).await
}
TestApp::start_live 把同一个应用连接到运行中的 Pulsar。
测试用 stand(示例自带的辅助函数)从 PULSAR_TEST_URL 读取地址,未设置时跳过。
RUSTSTREAM_REQUIRE_LIVE=1 把这种跳过变成失败,所以 CI 任务没有服务器就无法通过。
测试加入自己的订阅,订阅以进程命名。
新订阅从主题末尾开始,所以不会读到之前运行留下的任何东西。
断言与进程内的完全相同。
cargo test
PULSAR_TEST_URL=pulsar://localhost:6650 cargo test -- --test-threads=1
服务器就是快速开始中的那一个。