1
Generelt teknisk / Rust: How to test a module that uses TcpStream?
« på: 27. April 2023, 14:06 pm »
How do you make a module testable that requires a std::net::TcpStream?
I had this problem with a (toy) project of mine that used a struct to encapsulate a BufReader and a BufWriter, presenting only the following in order to make it impossible to accidentally leak data into or out of the raw stream:
My initial though was to use generics to allow any type of stream but this turned out to be both really hard (because std::net::TcpStream uses .try_clone() rather than implementing the Clone trait, and it uses .shutdown(Shutdown) instead of .close()) and it would have required all the modules that depended on this one to deal with the generics that weren't needed to begin with. What started as a simple problem was now becoming really complicated in my mind.
I could turn it around by saying that my component now just implements a new trait, but this would not only require a whole lot of extra code just to implement, it would still require all the other modules to deal with the extra generics and most importantly: it would actually bring me no closer to the original goal: Testing this really quite simple module right here.
During my evening walk, it finally dawned on me that the people who designed the IP stack already thought of this exact problem (as well as many other problems) decades ago and they implemented a solution so simple it's easy to forget about it and make things harder than they need be:
The loopback address.
Specifying a port number of 0 means the operating system gets to pick an unused port for you. So I simply added one more function, a double constructor that creates a pair of object instances connected to each other:
Not only did this make it trivial to test this module, it also made it dead simple to test all those other modules that depended on this one! The idea is easily transferred to other types of scenarios where you have to test code that uses TcpStream directly:
Final note: On certain very locked-down platforms you may have to relax local firewall rules in order to allow loopback traffic, but no sane person would ever test their network code on a machine anywhere near production anyway so if this turns out to be a problem then maybe you should re-evaluate how and where you do things.
I had this problem with a (toy) project of mine that used a struct to encapsulate a BufReader and a BufWriter, presenting only the following in order to make it impossible to accidentally leak data into or out of the raw stream:
Kode: [Velg]
pub fn new(TcpStream) -> Self
pub fn reader(&mut self) -> &mut BufReader<TcpStream>
pub fn writer(&mut self) -> &mut BufWriter<TcpStream>
pub fn close(&mut self)
My initial though was to use generics to allow any type of stream but this turned out to be both really hard (because std::net::TcpStream uses .try_clone() rather than implementing the Clone trait, and it uses .shutdown(Shutdown) instead of .close()) and it would have required all the modules that depended on this one to deal with the generics that weren't needed to begin with. What started as a simple problem was now becoming really complicated in my mind.
I could turn it around by saying that my component now just implements a new trait, but this would not only require a whole lot of extra code just to implement, it would still require all the other modules to deal with the extra generics and most importantly: it would actually bring me no closer to the original goal: Testing this really quite simple module right here.
During my evening walk, it finally dawned on me that the people who designed the IP stack already thought of this exact problem (as well as many other problems) decades ago and they implemented a solution so simple it's easy to forget about it and make things harder than they need be:
The loopback address.
Specifying a port number of 0 means the operating system gets to pick an unused port for you. So I simply added one more function, a double constructor that creates a pair of object instances connected to each other:
Kode: [Velg]
// Convenience function for testing
pub fn loopback() -> Result<(Self, Self), std::io::Error> {
let listener = TcpListener::bind("127.0.0.1:0")?;
// From viewpoint of the client
let server_addr = listener.local_addr()?;
let server = TcpStream::connect(server_addr)?;
// From viewpoint of the server
let (client, client_addr) = listener.accept()?;
let conn1 = Self::new(server, server_addr);
let conn2 = Self::new(client, client_addr);
return Ok((conn1, conn2));
}
Not only did this make it trivial to test this module, it also made it dead simple to test all those other modules that depended on this one! The idea is easily transferred to other types of scenarios where you have to test code that uses TcpStream directly:
Kode: [Velg]
#[cfg(test)]
mod tests {
use super::*;
fn loopback() -> Result<(std::net::TcpStream, std::net::TcpStream), std::io::Error> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let server = std::net::TcpStream::connect(listener.local_addr().unwrap())?;
let (client, _) = listener.accept()?;
return Ok((server, client));
}
// Actual tests here, just call loopback() whenever you need a pair of streams connected to each other
// Put data into one, read it out of the other.
// ...
}
Final note: On certain very locked-down platforms you may have to relax local firewall rules in order to allow loopback traffic, but no sane person would ever test their network code on a machine anywhere near production anyway so if this turns out to be a problem then maybe you should re-evaluate how and where you do things.