Тестирование 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()));
    }