I'm trying to test the following scenario:
-
A
and B
are structs.
- When I call
A.say_something(message)
, I want to make sure it sends the message to B
.
-
B
runs in its own thread separate to A
.
-
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).
-
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?
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:
-
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
-
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.
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.
Thanks for the replies , 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: Rust Playground
1 Like