Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.
Тестирование сервиса на файле или в конвейере
Тест запускает собственное приложение сервиса через TestApp на внутрипроцессном транспорте или на настоящем файле потока.
Тестируемый сервис оформляет заказы.
Заказ приходит под ключом orders, и обработчик отвечает квитанцией под ключом receipts.
Заказ без суммы отбрасывается.
Заказ сверх лимита проверки на мошенничество откладывается: обработчик просит доставить его снова через минуту с retry_after.
После трёх доставок заказ уходит под ключ orders.review, ключ dead-letter.
Приложение строит service(file), поэтому тест выбирает файл сам, а main берёт рабочий.
Сервис взят из самого файла примера, examples/guide/testing.rs.
Тестам нужна фича testing крейта ruststream-sea-file в [dev-dependencies].
Заказ и его квитанция
- Макросы
- Вручную
#[tokio::test]
async fn an_order_gets_a_receipt() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
an_order_gets_a_receipt_in(tb).await
}
async fn an_order_gets_a_receipt_in(tb: TestApp<()>) -> Result<(), Box<dyn Error>> {
tb.broker::<FileBroker>()
.message(&Order {
id: 1,
amount_cents: 2_500,
})
.publish()
.await?;
tb.broker::<FileBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.broker::<FileBroker>()
.published::<Receipt>("receipts")
.assert_called_once()
.with(&Receipt {
order: 1,
amount_cents: 2_500,
});
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn an_order_gets_a_receipt() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
an_order_gets_a_receipt_in(tb).await
}
async fn an_order_gets_a_receipt_in(tb: TestApp<()>) -> Result<(), Box<dyn Error>> {
tb.broker::<FileBroker>()
.message(&Order {
id: 1,
amount_cents: 2_500,
})
.publish()
.await?;
tb.broker::<FileBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.broker::<FileBroker>()
.published::<Receipt>("receipts")
.assert_called_once()
.with(&Receipt {
order: 1,
amount_cents: 2_500,
});
tb.shutdown().await?;
Ok(())
}
TestApp::start подключает FileBroker к журналу в памяти.
Файл по пути никогда не открывается, поэтому тесты работают параллельно в одном процессе.
Тест обращается к брокеру по его рабочему типу: tb.broker::<FileBroker>().
publish возвращает управление, когда обработчик и его ответ завершены.
Поэтому следующие строки проверяют уже готовое состояние: как подписчик завершил доставку и что дошло до receipts.
Тело теста принимает запущенное приложение аргументом, поэтому его же выполняет и живой тест ниже.
Заказ без суммы
- Макросы
- Вручную
#[tokio::test]
async fn an_order_without_an_amount_is_dropped() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<FileBroker>()
.message(&Order {
id: 2,
amount_cents: 0,
})
.publish()
.await?;
tb.broker::<FileBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.broker::<FileBroker>()
.published::<Receipt>("receipts")
.assert_not_called();
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn an_order_without_an_amount_is_dropped() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<FileBroker>()
.message(&Order {
id: 2,
amount_cents: 0,
})
.publish()
.await?;
tb.broker::<FileBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::drop());
tb.broker::<FileBroker>()
.published::<Receipt>("receipts")
.assert_not_called();
tb.shutdown().await?;
Ok(())
}
settled проверяет исход, который вернул обработчик.
assert_not_called проверяет, что квитанция не дописана.
Задержка без ожидания
- Макросы
- Вручную
#[tokio::test(start_paused = true)]
async fn an_order_held_three_times_goes_to_review() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
let large = Order {
id: 3,
amount_cents: 250_000,
};
tb.broker::<FileBroker>().message(&large).publish().await?;
tb.broker::<FileBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::retry_after(HOLD));
// The delay ends in a copy of the order, appended under `orders`.
tb.advance(HOLD).await?;
tb.broker::<FileBroker>()
.published::<Order>("orders")
.assert_called(2)
.with(&large)
.with_header(RETRY_COUNT_HEADER, "1");
// The third delivery is the last one: it is written under `orders.review` as it arrived.
tb.advance(HOLD).await?;
assert_eq!(
tb.broker::<FileBroker>().subscriber("orders").outcomes(),
[Outcome::Nack, Outcome::Nack, Outcome::Nack],
);
tb.broker::<FileBroker>()
.published::<Order>("orders.review")
.assert_called_once()
.with(&large);
tb.shutdown().await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn an_order_held_three_times_goes_to_review() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
let large = Order {
id: 3,
amount_cents: 250_000,
};
tb.broker::<FileBroker>().message(&large).publish().await?;
tb.broker::<FileBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::retry_after(HOLD));
// The delay ends in a copy of the order, appended under `orders`.
tb.advance(HOLD).await?;
tb.broker::<FileBroker>()
.published::<Order>("orders")
.assert_called(2)
.with(&large)
.with_header(RETRY_COUNT_HEADER, "1");
// The third delivery is the last one: it is written under `orders.review` as it arrived.
tb.advance(HOLD).await?;
assert_eq!(
tb.broker::<FileBroker>().subscriber("orders").outcomes(),
[Outcome::Nack, Outcome::Nack, Outcome::Nack],
);
tb.broker::<FileBroker>()
.published::<Order>("orders.review")
.assert_called_once()
.with(&large);
tb.shutdown().await?;
Ok(())
}
Тест начинается с остановленными часами, а advance сдвигает их вперёд.
Минута HOLD не стоит ничего.
По окончании задержки retry_after дописывает копию заказа под ключом orders.
Число попыток хранится в заголовке копии, и published показывает её под orders рядом с собственным заказом теста.
Третья доставка - последняя, которую допускает точка монтирования, поэтому заказ уходит под orders.review в том виде, в каком пришёл.
Каждая отложенная доставка записывается как Outcome::Nack.
Что моделируют внутрипроцессные транспорты
Файл потока внутри процесса - это журнал в памяти. Он новый для каждого значения брокера и общий для его клонов. Он хранит то же, что хранит файл:
- Каждый ключ потока нумеруется с единицы, и живая подписка начинает с конца журнала.
- Воспроизведение читает то, что хранит журнал, и завершается в его конце.
- Позиции и перемотка разрешаются по журналу с теми же отказами, что и на файле.
end_with_eosпри завершении работы заканчивает каждую подписку.
Журнал, который сервис читает при запуске, записывается через клон его брокера до TestApp::start, как это делают тесты руководства Возобновление и перемотка.
Канал внутри процесса - это стандартный ввод, который подаёт тест, и стандартный вывод, в который пишет сервис.
tb.broker::<StdioBroker>().message(..) - это строка, пришедшая под своим ключом, а published::<T>(key) читает строку, которую записала ступень.
Ступень читает собственный вывод только при loopback, как и в настоящем канале.
Оба транспорта отвергают то же, что и настоящие: ключ потока, который клиент не принимает, пустую строку в канале, интервал маяков, не кратный 1024.
Байты файла .ss, то, что файл сохраняет между запусками, и формат строки в настоящем канале видны только на настоящем транспорте.
Тот же тест на файле потока
- Макросы
- Вручную
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_order_gets_a_receipt_on_a_stream_file() -> Result<(), Box<dyn Error>> {
// A file of this run's own, so nothing an earlier run appended is read.
let path = env::temp_dir().join(format!("checkout-{}.ss", process::id()));
let file = FileBroker::new(path.to_string_lossy());
let tb = TestApp::start_live(service(file)).await?;
an_order_gets_a_receipt_in(tb).await?;
fs::remove_file(&path)?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_order_gets_a_receipt_on_a_stream_file() -> Result<(), Box<dyn Error>> {
// A file of this run's own, so nothing an earlier run appended is read.
let path = env::temp_dir().join(format!("manual-checkout-{}.ss", process::id()));
let file = FileBroker::new(path.to_string_lossy());
let tb = TestApp::start_live(service(file)).await?;
an_order_gets_a_receipt_in(tb).await?;
fs::remove_file(&path)?;
Ok(())
}
TestApp::start_live запускает то же приложение на настоящем файле потока во временном каталоге.
Транспорт - это локальная файловая система, поэтому живому тесту не нужен сервер, и он выполняется вместе с остальными в cargo test.
Файл называется по процессу, поэтому тест не читает ничего, что дописал прошлый прогон, а в конце тест удаляет файл.
Проверки те же, что и внутри процесса, без изменений.
Ступень конвейера проверяется вживую как дочерний процесс с перенаправленными стандартными вводом и выводом, как показывает руководство Конвейеры оболочки.