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



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!");
}
}
}

我正试图为这段代码写一个集成测试。很明显,我的测试它一直运行到无限远,因为std.

提供的TCP流的事件循环
// tests/server.rs
#[test]
fn run() {
server::run();
// the rest of the code's test...
}

在不更改公共接口的情况下测试服务器是否正常运行(不需要接收任何请求)的好方法是什么?

注意:没有断言,也没有任何类型的Result<T, E>,因为我甚至不知道如何设置测试用例的东西,它是无止境的。

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

Rust中的依赖注入通常是这样的:
  • 使用特征参数,而不是特定的对象类型
  • 创建对象的模拟,该对象也实现了所需的特性
  • 使用模拟在测试期间创建所需的行为

在我们的例子中,我将使用io::Read + io::Write来抽象TcpStream,因为这是我们使用的所有功能。如果你需要进一步的功能,而不仅仅是这两个,你可能必须实现自己的NetworkStream: Send + Synctrait或类似的,在其中你可以代理TcpStream的进一步功能。

我将使用的Mock是来自mockstream crate的SyncMockStream。

对于以下示例,您需要将mockstream添加到Cargo.toml:

[dev-dependencies]
mockstream = "0.0.3"

首先,这里是简单的版本,只有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

现在,如果我们需要进一步的功能,比如发送者地址,我们可以引入我们自己的特性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

注意,在这两种情况下,mockstream依赖项只需要作为dev-dependency。实际的cargo build不需要它


集成测试

如果你想进一步测试整个服务器,我会把服务器当作一个黑盒,而不是用一个外部工具,如behavior来测试它。

Behave是一个基于Python和Gherkin的行为测试框架,非常适合黑盒集成测试。

,您可以运行实际,unmockedcargo build生成可执行文件,然后用一个真正的测试实际功能连接。behave在这方面非常出色,特别是它在程序员和需求工程师之间架起了桥梁,因为实际的测试用例是书面的,非程序员可读的形式。

最新更新