multithreadingtestingrustdesign-patternsmocking

Mock subscriber of a threaded publisher in Rust


I am implementing an implementation of the Command Pattern where a subscriber would receive data from a reader thread and I have issues testing my implementation using Rust mocks. Consider this stripped down example:

use std::thread;

pub trait Subscriber: Sync + Send {
    fn callback(&self);
}

pub struct Publisher {
    thread_handler: Option<thread::JoinHandle<()>>,
}

impl Publisher {
    pub fn new() -> Self {
        Self {
            thread_handler: None,
        }
    }

    pub fn start(&mut self, sub: Box<dyn Subscriber>) {
        self.thread_handler = Some(thread::spawn(move || {
            sub.callback();
        }));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use mockall::*;

    mock! {
        TestSubscriber {}

        impl Subscriber for TestSubscriber {
            fn callback(&self);
        }
    }

    #[test]
    fn test_subscriber() {
        let mut publisher = Publisher::new();

        let mut mock_test_subscriber = Box::new(MockTestSubscriber::new());

        publisher.start(mock_test_subscriber);

        mock_test_subscriber.expect_callback().times(1);

        thread::sleep(std::time::Duration::from_millis(10));
    }
}

This throws

error[E0382]: borrow of moved value: `mock_test_subscriber`
  --> src/foo.rs:46:9
   |
42 |         let mut mock_test_subscriber = Box::new(MockTestSubscriber::new());
   |             ------------------------ move occurs because `mock_test_subscriber` has type `Box<MockTestSubscriber>`, which does not implement the `Copy` trait
43 |
44 |         publisher.start(mock_test_subscriber);
   |                           -------------------- value moved here
45 |
46 |         mock_test_subscriber.expect_callback().times(1);
   |         ^^^^^^^^^^^^^^^^^^^^ value borrowed here after move

I don't really know how to solve this, I tried wrapping the dyn Subscriber around Arc but still couldn't make it work. Any suggestions would be much appreciated. Or maybe there is a better design to achieve the same result.


Solution

  • You were on the right track trying to use Arc - it should be the first thing to try when doing ad-hoc multi-threading. Here since you are trying to share mock_test_subscriber across two threads, this is exactly what you need. So first we change the signature of Publisher::start to use Arc:

    pub fn start(&mut self, sub: Arc<dyn Subscriber>) {
        self.thread_handler = Some(thread::spawn(move || {
            sub.callback();
        }));
    }
    

    ... and update the call site accordingly:

    let mock_test_subscriber = Arc::new(MockTestSubscriber::new());
    // create another reference by cloning the Arc
    // then coerce the underlying concrete type into a trait object
    publisher.start(Arc::clone(&mock_test_subscriber) as Arc<dyn Subscriber>);
    

    Note that I removed the mut on mock_test_subscriber because it's probably not what you mean. Writing let mut foo = Arc::new(T); merely allows you to set foo to another Arc (i.e. point to something else), not modifying its underlying value. Arc is semantically a shared reference and therefore cannot be mutable due to Rust's rule on mutable aliasing. Syntactically, this is expressed as Arc not implementing DerefMut.

    To obtain a mutable reference to the underlying value, use Arc<Mutex<T>> or Arc<RwLock<T>>. Mutex and RwLock are able to safely provide mutable references through shared references by internally using the correct synchronisation primitives to ensure exclusive access.