Home > OS >  How do I use rust traits to abstract HTTP call for tests?
How do I use rust traits to abstract HTTP call for tests?

Time:07-02

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 our print_hello_world() function knows about.
  • we define a ConsolePrinter struct that we use at runtime to print the message. The ConsolePrinter of course has to implement HelloWorldPrinter to be usable with the print_hello_world() function.
  • for testing, we write the TestPrinter struct that we use instead of the ConsolePrinter. Instead of printing, it stores what it received so we can test whether it got passed the correct message. Of course, the ConsolePrinter also has to implement the HelloWorldPrinter trait to be usable with print_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.

  • Related