Тестирование TCP-сервера
Давайте перейдём к тестированию нашей функции handle_connection
.
Во-первых, нам нужен TcpStream
для работы. В сквозном или интеграционном тесте мы можем захотеть установить реальное TCP-соединение для проверки нашего кода. Одна из стратегий для этого — запустить приложение на порту 0 localhost
. Порт 0 не является допустимым портом UNIX, но он подойдёт для тестирования. Операционная система выберет для нас открытый порт TCP.
Вместо этого в этом примере мы напишем модульный тест для обработчика соединения, чтобы проверить, что для входных данных возвращаются правильные ответы. Чтобы наш модульный тест оставался изолированным и детерминированным, мы замокаем TcpStream
.
Для начала, мы изменим сигнатуру handle_connection
, чтобы упростить тестирование. handle_connection
на самом деле не требует async_std::net::TcpStream
, а требует любую структуру, которая реализует async_std::io::Read
, async_std::io::Write
и marker::Unpin
. Изменив сигнатуру типа таким образом, мы сможем передать мок для тестирования.
use async_std::io::{Read, Write};
async fn handle_connection(mut stream: impl Read + Write + Unpin) {
Далее давайте создадим мок TcpStream
, который реализует нужные типажи. Во-первых, давайте реализуем типаж Read
с методом poll_read
. Наш мок TcpStream
будет содержать некоторые данные, которые копируются в буфер чтения, и мы вернём Poll::Ready
, чтобы показать, что чтение завершено.
use super::*;
use futures::io::Error;
use futures::task::{Context, Poll};
use std::cmp::min;
use std::pin::Pin;
struct MockTcpStream {
read_data: Vec<u8>,
write_data: Vec<u8>,
}
impl Read for MockTcpStream {
fn poll_read(
self: Pin<&mut Self>,
_: &mut Context,
buf: &mut [u8],
) -> Poll<Result<usize, Error>> {
let size: usize = min(self.read_data.len(), buf.len());
buf[..size].copy_from_slice(&self.read_data[..size]);
Poll::Ready(Ok(size))
}
}
Наша реализация Write
очень похожа, хотя нам нужно написать три метода: poll_write
, poll_flush
и poll_close
. poll_write
скопирует входные данные в мок TcpStream
и вернёт Poll::Ready
после завершения. Для сброса или закрытия мока TcpStream
не требуется никакой работы, поэтому poll_flush
и poll_close
могут просто вернуть Poll::Ready
.
impl Write for MockTcpStream {
fn poll_write(
mut self: Pin<&mut Self>,
_: &mut Context,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
self.write_data = Vec::from(buf);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
}
Наконец, нашему моку нужно будет реализовать Unpin
, что означает, что его местоположение в памяти может быть безопасно перемещено. Для получения дополнительной информации о закреплении и Unpin
см. раздел о закреплении .
impl Unpin for MockTcpStream {}
Теперь мы готовы протестировать функцию handle_connection
. После настройки MockTcpStream
, содержащего некоторые начальные данные, мы можем запустить handle_connection
, используя атрибут #[async_std::test]
, аналогично тому, как мы использовали #[async_std::main]
. Чтобы убедиться, что handle_connection
работает должным образом, мы проверим, что в MockTcpStream
были записаны правильные данные на основе его исходного содержимого.
use std::fs;
#[async_std::test]
async fn test_handle_connection() {
let input_bytes = b"GET / HTTP/1.1\r\n";
let mut contents = vec![0u8; 1024];
contents[..input_bytes.len()].clone_from_slice(input_bytes);
let mut stream = MockTcpStream {
read_data: contents,
write_data: Vec::new(),
};
handle_connection(&mut stream).await;
let expected_contents = fs::read_to_string("hello.html").unwrap();
let expected_response = format!("HTTP/1.1 200 OK\r\n\r\n{}", expected_contents);
assert!(stream.write_data.starts_with(expected_response.as_bytes()));
}