Тестирование
Цель этой страницы - дать рекомендации по написанию полезных модульных тестов в асинхронных приложениях.
Приостановка и возобновление времени в тестах
Иногда асинхронный код явно ожидает, вызывая tokio::time::sleep или ожидая tokio::time::Interval::tick. Тестирование поведения, основанного на времени (например, экспоненциальной отсрочки), может стать громоздким, когда модульный тест начинает работать очень медленно. Однако внутренне функциональность, связанная со временем в tokio, поддерживает приостановку и возобновление времени. Приостановка времени имеет эффект, что любой future, связанный со временем, может стать готовым досрочно. Условием досрочного разрешения future, связанного со временем, является то, что больше нет других future, которые могут стать готовыми. Это по сути перематывает время вперед, когда единственный ожидаемый future связан со временем:
#![allow(unused)] fn main() { #[tokio::test] async fn paused_time() { tokio::time::pause(); let start = std::time::Instant::now(); tokio::time::sleep(Duration::from_millis(500)).await; println!("{:?}ms", start.elapsed().as_millis()); } }
Этот код выводит 0ms на разумной машине.
Для модульных тестов часто полезно запускать с приостановленным временем на протяжении всего теста. Это можно достичь, просто установив аргумент макроса start_paused в true:
#![allow(unused)] fn main() { #[tokio::test(start_paused = true)] async fn paused_time() { let start = std::time::Instant::now(); tokio::time::sleep(Duration::from_millis(500)).await; println!("{:?}ms", start.elapsed().as_millis()); } }
Имейте в виду, что атрибут start_paused требует функции tokio test-util.
Смотрите tokio::test "Configure the runtime to start with time paused" для более подробной информации.
Конечно, временной порядок разрешения future сохраняется, даже при использовании разных future, связанных со временем:
#![allow(unused)] fn main() { #[tokio::test(start_paused = true)] async fn interval_with_paused_time() { let mut interval = interval(Duration::from_millis(300)); let _ = timeout(Duration::from_secs(1), async move { loop { interval.tick().await; println!("Tick!"); } }) .await; } }
Этот код немедленно выводит "Tick!" ровно 4 раза.
Мокирование с использованием AsyncRead и AsyncWrite
Общие трейты для асинхронного чтения и записи (AsyncRead и AsyncWrite) реализованы, например, сокетами. Они могут быть использованы для мокирования I/O, выполняемого сокетом.
Рассмотрим для настройки этот простой цикл TCP-сервера:
use tokio::net::TcpListener; #[tokio::main] async fn main() { if true { return } let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); loop { let Ok((mut socket, _)) = listener.accept().await else { eprintln!("Failed to accept client"); continue; }; tokio::spawn(async move { let (reader, writer) = socket.split(); // Запускаем некоторый обработчик клиентского соединения, например: // handle_connection(reader, writer) // .await // .expect("Failed to handle connection"); }); } }
Здесь каждое TCP-клиентское соединение обслуживается своей выделенной задачей tokio. Эта задача владеет reader и writer, которые разделены из TcpStream.
Теперь рассмотрим саму задачу обработки клиента, особенно where-клаузу сигнатуры функции:
#![allow(unused)] fn main() { use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; async fn handle_connection<Reader, Writer>( reader: Reader, mut writer: Writer, ) -> std::io::Result<()> where Reader: AsyncRead + Unpin, Writer: AsyncWrite + Unpin, { let mut line = String::new(); let mut reader = BufReader::new(reader); loop { if let Ok(bytes_read) = reader.read_line(&mut line).await { if bytes_read == 0 { break Ok(()); } writer .write_all(format!("Thanks for your message.\r\n").as_bytes()) .await .unwrap(); } line.clear(); } } }
По сути, данные reader и writer, которые реализуют AsyncRead и AsyncWrite, обслуживаются последовательно. Для каждой полученной строки обработчик отвечает "Thanks for your message.".
Для модульного тестирования обработчика клиентского соединения можно использовать tokio_test::io::Builder как мок:
#![allow(unused)] fn main() { use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; async fn handle_connection<Reader, Writer>( reader: Reader, mut writer: Writer, ) -> std::io::Result<()> where Reader: AsyncRead + Unpin, Writer: AsyncWrite + Unpin, { let mut line = String::new(); let mut reader = BufReader::new(reader); loop { if let Ok(bytes_read) = reader.read_line(&mut line).await { if bytes_read == 0 { break Ok(()); } writer .write_all(format!("Thanks for your message.\r\n").as_bytes()) .await .unwrap(); } line.clear(); } } #[tokio::test] async fn client_handler_replies_politely() { let reader = tokio_test::io::Builder::new() .read(b"Hi there\r\n") .read(b"How are you doing?\r\n") .build(); let writer = tokio_test::io::Builder::new() .write(b"Thanks for your message.\r\n") .write(b"Thanks for your message.\r\n") .build(); let _ = handle_connection(reader, writer).await; } }