What is the Rust way to test interaction?


#1

I’m trying to test the following scenario:

  1. A and B are structs.
  2. When I call A.say_something(message), I want to make sure it sends the message to B.
  3. B runs in its own thread separate to A.
  4. B can be heavy to set up, so I’d like to be able to mock it (or at least not use the full implementation).
  5. A is what users will construct, and it would be nice if they can call A::new().

In the real world, B is an endpoint, and A is an adaptor that transforms the message to what B understands.

Minimal example

playpen

use std::sync::mpsc::{self, Sender, Receiver};
use std::thread;
use std::time::Duration;

struct B {
    rx: Receiver<String>,
}

impl B {
    pub fn listen(&self) {
        if let Ok(message) = self.rx.recv() {
            println!("{}", message);
        }
    }
}

struct A {
    tx: Sender<String>,
}

impl A {
    pub fn new() -> Self {
        let (tx, rx) = mpsc::channel();
        let b = B { rx: rx };
        thread::spawn(move || b.listen());

        A { tx: tx }
    }

    pub fn say_something(&self, message: String) {
        self.tx.send(message).unwrap();
    }
}

fn main() {
    let a = A::new();
    a.say_something("I'm giving up on you".to_string());
    thread::sleep(Duration::from_millis(200)); // wait for B to print message out
}

What is the Rust way to write tests for A.say_something(..)?

  • Extract the construction / thread-spawn of B into a trait function, and in the tests, pass in a different function that takes control of the Receiver instead of spawning a B
  • Something else?

#2

By no means do I have the cleanest code, or possibly the best answer to this. In fact, there are probably some great libraries for this, that being said:

  1. I’ve found writing these as integration tests to be best, i.e. in the top-level tests/ path and an individual module (file) per test, with any common things being shared through a submodule tests/common/mod.rs

  2. Spawning threads in Rust is easy, just do it in your test, for example you can look here:

https://github.com/bluejekyll/trust-dns/blob/master/server/tests/server_future_tests.rs#L41

That spawns a server thread, and then runs a common function for the client query.


#3

More specifically to your question, I realize you are asking a question that could have a firmer answer. If you make some of your stuff Trait based, you can test a lot of this passing in a custom test only implementation that has a barrier or other lock to validate that something specific happened after a client request in the other thread.

In this example:

https://github.com/bluejekyll/trust-dns/blob/master/server/tests/client_tests.rs#L53

I have a custom implementation of the ClientConnection which is a non-network based connection, which allows me to test the logic without any pesky sockets getting in the way.


#4

Thanks for the replies :slight_smile:, and I managed to come up with the following by looking at the client test:

impl A {
    pub fn new() -> Self {
        let (tx, rx) = mpsc::channel();
        let b_spawn = || { thread::spawn(|| B { rx: rx }.listen()); };
        A::internal_new(tx, b_spawn)
    }

    fn internal_new<F>(tx: Sender<String>, b_spawn: F) -> Self
        where F: FnOnce()
    {
        b_spawn();
        A { tx: tx }
    }

    // ...
}

Actual code vs test code can then use the different new() functions like so:

fn main() {
    let a = A::new();
    a.say_something("I'm giving up on you".to_string());
    thread::sleep(Duration::from_millis(200)); // wait for B to print message out
}

mod test {
    use std::sync::mpsc;
    use super::A;

    #[test]
    fn says_something_to_B() {
        let (tx, rx) = mpsc::channel();
        let a = A::internal_new(tx, || {});
        a.say_something("I'm giving up on you".to_string());

        assert_eq!(Ok("I'm giving up on you".to_string()), rx.recv());
    }
}

playpen: https://is.gd/Mm4ZDr