Home > Software engineering >  Unit testing a service that accepts an Fn closure as a callback
Unit testing a service that accepts an Fn closure as a callback

Time:01-27

I have the following service that registers callbacks to execute at a certain epoch, identified by an i64. The service has a vector of callbacks (that are bounded by the Send Fn() -> () traits). Each callback can be executed multiple times (hence Fn instead of FnOnce or FnMut). The Send trait is needed because the callbacks will be registered by other threads, and this service will run in the background.

So far so good, but I'd like to test that the callbacks are executed the way they should be (i.e. the i64 epoch ticking in some direction which may (or may not) cause the callback to be executed). The problem is that I cannot seem to be able to think of a way to achieve this. I'm coming from Golang in which it is quite easy to inject a mock callback and assert whether it was called since such limitations are not imposed by the compiler, however when I employ the same methods in Rust, I end up with an FnMut instead of an Fn.

use std::sync::{Arc, Mutex};
use std::collections::HashMap;

struct Service<T: Send   Fn() -> ()> {
    triggers: Arc<Mutex<HashMap<i64, Vec<Box<T>>>>>,
}

impl<T: Send   Fn() -> ()> Service<T> {
    pub fn build() -> Self {
        Service {
            triggers: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub fn poll(&'static self) {
        let hs = Arc::clone(&self.triggers);
        tokio::spawn(async move {
            loop {
                // do some stuff and get `val`
                if let Some(v) = hs.lock().unwrap().get(&val) {
                    for cb in v.iter() {
                        cb();
                    }
                }
            }
        });
        ()
    }

    pub fn register_callback(&self, val: i64, cb: Box<T>) -> () {
        self.triggers
            .lock()
            .unwrap()
            .entry(val)
            .or_insert(Vec::new())
            .push(cb);
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn test_poll() {
        let c = Service::build();
        let mut called = false;
        let cb = || called = true;
        let h: i64 = 10;
        c.register_callback(h, Box::new(cb));
        assert_eq!(called, false);
    }
}

Any ideas on how would this sort of behavior could be tested in Rust? The only thing I can think of is perhaps some channel that would pass a local value to the test and relinquish ownership over it?

CodePudding user response:

The best way would probably be to make your interface as general as possible:

// type bounds on structs are generally unnecessary so I removed it here.
struct Service<T> {
    triggers: Arc<Mutex<HashMap<i64, Vec<Box<T>>>>>,
}

impl<T: Send   FnMut() -> ()> Service<T> {
    pub fn build() -> Self {
        Service {
            triggers: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub fn poll(&'static self, val: i64) {
        let hs = Arc::clone(&self.triggers);
        tokio::spawn(async move {
            loop {
                // do some stuff and get `val`
                if let Some(v) = hs.lock().unwrap().get_mut(&val) {
                    for cb in v.iter_mut() {
                        cb();
                    }
                }
            }
        });
        ()
    }

    pub fn register_callback(&self, val: i64, cb: Box<T>) -> () {
        self.triggers
            .lock()
            .unwrap()
            .entry(val)
            .or_insert(Vec::new())
            .push(cb);
    }
}

But if you can't generalize the interface you can just use an AtomicBool like this:

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{Ordering, AtomicBool};

    #[test]
    fn test_poll() {
        let c = Service::build();
        let mut called = AtomicBool::new(false);
        let cb = || called.store(true, Ordering::Relaxed);
        let h: i64 = 10;
        c.register_callback(h, Box::new(cb));
        assert!(!called.load(Ordering::Relaxed));
    }
}
  • Related