Coming from Go there are a lot of interfaces you can use to do something like the below:
async fn get_servers(client: &dyn std::marker::Send) -> Result<String, impl std::error::Error> {
let servers_str = client.send().await?.text()
let v: Value = serde_json::from_str(servers_str)?;
println!("{:?}", v);
Ok(servers_str.to_string())
}
// ...
get_servers(client.get(url))
I could pass in something that just implemented the send and return the text. That way makes the code testable. I thought maybe the send auto trait would do that but apparently not. Says send not found. Maybe some kind of impl requestbuilder?
CodePudding user response:
In general, this is absolutely possible and (correct me if I'm wrong) even advised. It's a programming paradigm called dependency injection.
Simplified, this means in your case, pass in the dependent object via an interface (or in Rust: trait) so you can replace it at test time with an object of a different type.
Your mistake here is that the std::marker::Send
trait does not what you think it does; it marks objects for being transferrable between threads. It's closely linked to std::marker::Sync
, meaning, it can be accessed by multiple threads without causing race conditions.
While many libraries already have traits you can use for that purpose, in a lot of cases you will have to set up your own trait. Here, for example, we have a hello world function, that gets tested by replacing its printer with a different one, specialized for testing. We achieve that by passing the printer into the hello world function through the abstraction of a trait, as already mentioned.
trait HelloWorldPrinter {
fn print_text(&mut self, msg: &str);
}
struct ConsolePrinter;
impl HelloWorldPrinter for ConsolePrinter {
fn print_text(&mut self, msg: &str) {
println!("{}", msg);
}
}
// This is the function we want to test.
// Note that we are using a trait here so we can replace the actual
// printer with a test mock when testing.
fn print_hello_world(printer: &mut impl HelloWorldPrinter) {
printer.print_text("Hello world!");
}
fn main() {
let mut printer = ConsolePrinter;
print_hello_world(&mut printer);
}
#[cfg(test)]
mod tests {
use super::*;
struct TestPrinter {
messages: Vec<String>,
}
impl TestPrinter {
fn new() -> Self {
Self { messages: vec![] }
}
}
impl HelloWorldPrinter for TestPrinter {
fn print_text(&mut self, msg: &str) {
self.messages.push(msg.to_string());
}
}
#[test]
fn prints_hello_world() {
let mut printer = TestPrinter::new();
print_hello_world(&mut printer);
assert_eq!(printer.messages, ["Hello world!"]);
}
}
When doing cargo run
:
Hello world!
When doing cargo test
:
Running unittests src/main.rs
running 1 test
test tests::prints_hello_world ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
As a little explanation, if that code doesn't explain itself:
- we create a trait
HelloWorldPrinter
whic his the only thing ourprint_hello_world()
function knows about. - we define a
ConsolePrinter
struct that we use at runtime to print the message. TheConsolePrinter
of course has to implementHelloWorldPrinter
to be usable with theprint_hello_world()
function. - for testing, we write the
TestPrinter
struct that we use instead of theConsolePrinter
. Instead of printing, it stores what it received so we can test whether it got passed the correct message. Of course, theConsolePrinter
also has to implement theHelloWorldPrinter
trait to be usable withprint_hello_world()
.
I hope that goes into the direction of your question. If you have any questions, feel free to discuss further.
I can't directly tell you what you should write to solve your problem, as your question is quite vague, but this should be the toolset you need to solve your problem. I hope.