如何为HTTP服务器事件循环编写集成测试?

发布于 2025-02-12 19:38:17 字数 648 浏览 1 评论 0原文

上以的方式为最终项目的基本示例:

use std::net::TcpListener;

mod server {
    fn run() {
        let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

        for stream in listener.incoming() {
            let stream = stream.unwrap();

            println!("Connection established!");
        }
    }
}

我试图为该代码编写集成测试。 显然,我的测试正在运行到无限之外,因为TCP流提供的事件循环。

// tests/server.rs

#[test]
fn run() {
    server::run();

    // the rest of the code's test...
}

什么是测试服务器正确站立的好方法(无需收到任何请求),而无需更改公共接口以进行测试?

注意:没有任何类型的结果< t,e>都没有断言,因为我什至不知道如何为其运行的某些内容设置测试案例。

Taking the base example for the final project on The Book:

use std::net::TcpListener;

mod server {
    fn run() {
        let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

        for stream in listener.incoming() {
            let stream = stream.unwrap();

            println!("Connection established!");
        }
    }
}

I am trying to write an integration test for that piece of code.
Obviously, my test it's running to the infinite and beyond because the event loop of the TCP stream provided by the std.

// tests/server.rs

#[test]
fn run() {
    server::run();

    // the rest of the code's test...
}

What would be a good way to test that the server stands up correctly (without need to receive any request) without change the public interface for just testing purposes?

NOTE: There's no assertions neither Result<T, E> of any type because I didn't even know how to set up the test case for something that it's running endless.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

风向决定发型 2025-02-19 19:38:17

我不会从Rust内测试整个服务器,而是测试它的组件。我要测试的最高组件是一个连接。

RUST中的依赖性注入通常是这样工作的:

  • 使用特征对于参数而不是特定对象类型
  • 创建对象的模拟,该对象也实现了所需的特征,
  • 使用模拟在我们的情况下在测试中创建所需的行为

,我将使用> io ::读取 + io ::写摘要tcpstream,因为这是我们使用的所有拟合。如果您需要进一步的功能,而不仅仅是这两个功能,则可能必须实现自己的networkStream:send + sync特征或类似性,您可以在其中代理tcpstream的进一步功能。 。

我将使用的模拟是 syncmockstream> syncmockstream href =“ https://crates.io/crates/mockstream/0.0.3” rel =“ nofollow noreferrer”> oberstream 板条箱。

对于以下示例,您需要将oigsstream添加到您的cargo.toml

[dev-dependencies]
mockstream = "0.0.3"

首先,这是一个更简单的版本,只有io ::读取 + io

mod server {
    use std::{io, net::TcpListener};

    fn handle_connection(
        mut stream: impl io::Read + io::Write,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        println!("Connection established!");

        // Read 'hello'
        let mut buf = [0u8; 5];
        stream.read_exact(&mut buf)?;
        if &buf != b"hello" {
            return Err(format!("Received incorrect data: '{:?}'", buf).into());
        }

        println!("Received 'hello'. Sending 'world!' ...");

        // Respond with 'world!'
        stream.write_all(b"world!\n")?;
        stream.flush()?;

        println!("Communication finished. Closing connection ...");

        Ok(())
    }

    pub fn run(addr: &str) {
        let listener = TcpListener::bind(addr).unwrap();

        for stream in listener.incoming() {
            let stream = stream.unwrap();

            std::thread::spawn(move || {
                if let Err(e) = handle_connection(stream) {
                    println!("Connection closed with error: {}", e);
                } else {
                    println!("Connection closed.");
                }
            });
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        use mockstream::SyncMockStream;
        use std::time::Duration;

        #[test]
        fn hello_world_handshake() {
            // Arrange
            let mut stream = SyncMockStream::new();
            let connection = stream.clone();
            let connection_thread = std::thread::spawn(move || handle_connection(connection));

            // Act
            stream.push_bytes_to_read(b"hello");
            std::thread::sleep(Duration::from_millis(100));

            // Assert
            assert_eq!(stream.pop_bytes_written(), b"world!\n");
            connection_thread.join().unwrap().unwrap();
        }
    }
}

fn main() {
    server::run("127.0.0.1:7878");
}
> nc localhost 7878
hello
world!
> cargo test
running 1 test
test server::tests::hello_world_handshake ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

现在,如果我们需要进一步的功能,例如发件人地址,我们可以介绍自己的特质networkStream

mod traits {
    use std::io;

    pub trait NetworkStream: io::Read + io::Write {
        fn peer_addr_str(&self) -> io::Result<String>;
    }

    impl NetworkStream for std::net::TcpStream {
        fn peer_addr_str(&self) -> io::Result<String> {
            self.peer_addr().map(|addr| addr.to_string())
        }
    }
}

mod server {
    use crate::traits::NetworkStream;
    use std::net::TcpListener;

    fn handle_connection(
        mut stream: impl NetworkStream,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        println!("Connection established!");

        // Read 'hello'
        let mut buf = [0u8; 5];
        stream.read_exact(&mut buf)?;
        if &buf != b"hello" {
            return Err(format!("Received incorrect data: '{:?}'", buf).into());
        }

        println!("Received 'hello'. Sending response ...");

        // Respond with 'world!'
        stream.write_all(format!("hello, {}!\n", stream.peer_addr_str()?).as_bytes())?;
        stream.flush()?;

        println!("Communication finished. Closing connection ...");

        Ok(())
    }

    pub fn run(addr: &str) {
        let listener = TcpListener::bind(addr).unwrap();

        for stream in listener.incoming() {
            let stream = stream.unwrap();

            std::thread::spawn(move || {
                if let Err(e) = handle_connection(stream) {
                    println!("Connection closed with error: {}", e);
                } else {
                    println!("Connection closed.");
                }
            });
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        use mockstream::SyncMockStream;
        use std::time::Duration;

        impl crate::traits::NetworkStream for SyncMockStream {
            fn peer_addr_str(&self) -> std::io::Result<String> {
                Ok("mock".to_string())
            }
        }

        #[test]
        fn hello_world_handshake() {
            // Arrange
            let mut stream = SyncMockStream::new();
            let connection = stream.clone();
            let connection_thread = std::thread::spawn(move || handle_connection(connection));

            // Act
            stream.push_bytes_to_read(b"hello");
            std::thread::sleep(Duration::from_millis(100));

            // Assert
            assert_eq!(stream.pop_bytes_written(), b"hello, mock!\n");
            connection_thread.join().unwrap().unwrap();
        }
    }
}

fn main() {
    server::run("127.0.0.1:7878");
}
> nc localhost 7878
hello
hello, 127.0.0.1:50718!
> cargo test
running 1 test
test server::tests::hello_world_handshake ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

请注意,在这两种情况下,oigs> oigsstream依赖关系仅是需要作为dev依赖性。实际的货物构建不需要它。


集成测试

如果您想进一步测试整个服务器,我将把服务器视为黑匣子,然后使用外部工具进行测试,例如表现

行为是基于Python和Gherkin的行为测试框架,非常适合黑匣子集成测试。

有了它,您可以运行货物构建产生的实际,无法锁定的可执行文件,然后使用真实连接测试实际功能。 表现对此非常出色,尤其是在弥合程序员和需求工程师之间的差距的情况下,因为实际的测试用例以书面的,非编程器可读的形式。

I wouldn't test the entire server from within Rust, I'd instead test components of it. The highest component I would test is a single connection.

Dependency injection in Rust usually works like this:

  • Use traits for parameters instead of specific object types
  • Create a mock of the object that also implements the desired trait
  • Use the mock to create the desired behaviour during tests

In our case, I will use io::Read + io::Write to abstract TcpStream, as that is all the funcionality we use. If you need further functionality instead of just those two, you might have to implement your own NetworkStream: Send + Sync trait or similar, in which you can proxy further functionality of TcpStream.

The Mock I will be using is SyncMockStream from the mockstream crate.

For the following examples you need to add mockstream to your Cargo.toml:

[dev-dependencies]
mockstream = "0.0.3"

First, here is the simpler version with just io::Read + io::Write:

mod server {
    use std::{io, net::TcpListener};

    fn handle_connection(
        mut stream: impl io::Read + io::Write,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        println!("Connection established!");

        // Read 'hello'
        let mut buf = [0u8; 5];
        stream.read_exact(&mut buf)?;
        if &buf != b"hello" {
            return Err(format!("Received incorrect data: '{:?}'", buf).into());
        }

        println!("Received 'hello'. Sending 'world!' ...");

        // Respond with 'world!'
        stream.write_all(b"world!\n")?;
        stream.flush()?;

        println!("Communication finished. Closing connection ...");

        Ok(())
    }

    pub fn run(addr: &str) {
        let listener = TcpListener::bind(addr).unwrap();

        for stream in listener.incoming() {
            let stream = stream.unwrap();

            std::thread::spawn(move || {
                if let Err(e) = handle_connection(stream) {
                    println!("Connection closed with error: {}", e);
                } else {
                    println!("Connection closed.");
                }
            });
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        use mockstream::SyncMockStream;
        use std::time::Duration;

        #[test]
        fn hello_world_handshake() {
            // Arrange
            let mut stream = SyncMockStream::new();
            let connection = stream.clone();
            let connection_thread = std::thread::spawn(move || handle_connection(connection));

            // Act
            stream.push_bytes_to_read(b"hello");
            std::thread::sleep(Duration::from_millis(100));

            // Assert
            assert_eq!(stream.pop_bytes_written(), b"world!\n");
            connection_thread.join().unwrap().unwrap();
        }
    }
}

fn main() {
    server::run("127.0.0.1:7878");
}
> nc localhost 7878
hello
world!
> cargo test
running 1 test
test server::tests::hello_world_handshake ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

Now if we need further functionality, like the sender address, we can introduce our own trait NetworkStream:

mod traits {
    use std::io;

    pub trait NetworkStream: io::Read + io::Write {
        fn peer_addr_str(&self) -> io::Result<String>;
    }

    impl NetworkStream for std::net::TcpStream {
        fn peer_addr_str(&self) -> io::Result<String> {
            self.peer_addr().map(|addr| addr.to_string())
        }
    }
}

mod server {
    use crate::traits::NetworkStream;
    use std::net::TcpListener;

    fn handle_connection(
        mut stream: impl NetworkStream,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        println!("Connection established!");

        // Read 'hello'
        let mut buf = [0u8; 5];
        stream.read_exact(&mut buf)?;
        if &buf != b"hello" {
            return Err(format!("Received incorrect data: '{:?}'", buf).into());
        }

        println!("Received 'hello'. Sending response ...");

        // Respond with 'world!'
        stream.write_all(format!("hello, {}!\n", stream.peer_addr_str()?).as_bytes())?;
        stream.flush()?;

        println!("Communication finished. Closing connection ...");

        Ok(())
    }

    pub fn run(addr: &str) {
        let listener = TcpListener::bind(addr).unwrap();

        for stream in listener.incoming() {
            let stream = stream.unwrap();

            std::thread::spawn(move || {
                if let Err(e) = handle_connection(stream) {
                    println!("Connection closed with error: {}", e);
                } else {
                    println!("Connection closed.");
                }
            });
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        use mockstream::SyncMockStream;
        use std::time::Duration;

        impl crate::traits::NetworkStream for SyncMockStream {
            fn peer_addr_str(&self) -> std::io::Result<String> {
                Ok("mock".to_string())
            }
        }

        #[test]
        fn hello_world_handshake() {
            // Arrange
            let mut stream = SyncMockStream::new();
            let connection = stream.clone();
            let connection_thread = std::thread::spawn(move || handle_connection(connection));

            // Act
            stream.push_bytes_to_read(b"hello");
            std::thread::sleep(Duration::from_millis(100));

            // Assert
            assert_eq!(stream.pop_bytes_written(), b"hello, mock!\n");
            connection_thread.join().unwrap().unwrap();
        }
    }
}

fn main() {
    server::run("127.0.0.1:7878");
}
> nc localhost 7878
hello
hello, 127.0.0.1:50718!
> cargo test
running 1 test
test server::tests::hello_world_handshake ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

Note that in both cases, the mockstream dependency is only needed as a dev-dependency. The actual cargo build does not require it.


Integration testing

If you want to go further up and test the entire server, I would treat the server as a black box instead and test it with an external tool like behave.

Behave is a behaviour test framework based on Python and Gherkin which is great for black box integration tests.

With it, you can run the actual, unmocked executable that cargo build produces, and then test actual functionality with a real connection. behave is excellent with that, especially in the regard that it bridges the gap between programmers and requirement engineers, as the actual test cases are in written, non-programmer-readable form.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文