How would I unit test a callback (function ptr)


#1

I’ve been tasked with building a library (crate) that contains a method another crate calls. The requirement is my method has to call a callback to return data to them (unfortunately this the way it will be). I want to unit test my method. I want to test that I correctly call the callback. I’m not sure how I can do this and was hoping someone had some ideas to try.

here’s the code:

// this callback is defined for testing purpose, so I have control over this method
// it must match a signature of extern "C" func_name(data : String)
extern "C" fn test_callback(data : String) {
    println!("got data '{}'", data);
}

// this method I implement in my library someone else will invoke. Its purpose is 
// to generate "stuff" and once "stuff" is generated, call the callback
pub extern "C" fn my_crate_method(cb: Option<extern fn(stuff : String)>) {

    let the_stuff = "This is the stuff".to_string();
    match cb {
        Some(f) => f(the_stuff),
        None => panic!("the callback was null"),
    };
}

// this method is in a crate which will consume my library (crate)
// they call my_crate_method
fn someone_elses_crate_method_that_calls_my_crate_method() {

    // for this example I'm calling test_callback but in practice
    // the callback would be their crate, not mine
    // eg:  my_crate_method(Some(someone_elses_callback));

    my_crate_method(Some(test_callback));
}

I’ve been looking at closures to make my test callback more local in scope. I didn’t see how I could do that. I looked at threading and mutexes and a few of those things. Not quite sure how to proceed.

how do I unit test my_crate_method successfully calls the passed in function?

I hope my question is clear. Thanks for the help.
Matt


#2

Not super clear what you mean. But do note that test_callback can be a nested function, e.g.:

// this method is in a crate which will consume my library (crate)
// they call my_crate_method
fn someone_elses_crate_method_that_calls_my_crate_method() {
    extern "C" fn test_callback(data : String) {
         println!("got data '{}'", data);
    }
    // for this example I'm calling test_callback but in practice
    // the callback would be their crate, not mine
    // eg:  my_crate_method(Some(someone_elses_callback));

    my_crate_method(Some(test_callback));
}

#3

One option would be to use a static variable inside your unit test module:

#[cfg(test)]
mod my_crate_method {
    use super::my_crate_method;

    static mut STUFF: Option<String> = None;

    extern "C" fn callback(s: String) {
        unsafe { STUFF = Some(s) };
    }

    #[test]
    fn it_works() {
        assert_eq!(unsafe { &STUFF }, &None);
        my_crate_method(Some(callback));
        assert_eq!(unsafe { &STUFF }, &Some(String::from("This is the stuff")));
    }
}

I would use different static variables for each unit test (by putting each unit test into its own module like above), in order to ensure that the variables are never accessed simultaneously from two different threads. With that, I believe the unsafe blocks above are safe.

If you can change the signature of the callback and avoid the extern "C" part, then you could use a simpler approach with a closure:

pub extern "C" fn takes_closure<F: Fn(String)>(cb: Option<F>) {
    // as my_create_method
}

#[cfg(test)]
mod takes_closure {
    use super::takes_closure;
    use std::cell::RefCell;

    #[test]
    fn it_works() {
        let mut s = RefCell::new(String::new());
        takes_closure(Some(|stuff: String| s.borrow_mut().push_str(&stuff)));
        assert_eq!(s.into_inner(), "This is the stuff");
    }
}

I was not able to find a way to use a closure with extern "C" in the signature.

The second method is basically the same as the example on interios mutability in the Rust Book.


#4

Thank you @mgeisler. I could not change the function signature is the purpose is it will be called by code (which my someone_elses_crate_method_t… method emuluates) I would not have access to. I also looked at the route @vitalyd suggested and was having problems with “global” data. You have answered that for me. I didn’t think about using Option.

Thank you
Matt


#5

Here’s another suggestion (rough sketch to show bare essentials):

trait Callback {
    fn call(self, s: String);
}

impl Callback for extern "C" fn(String) {
    fn call(self, s: String) {
        (self)(s)
    }
}

impl<F> Callback for F
where
    F: FnOnce(String),
{
    fn call(mut self, s: String) {
        (self)(s)
    }
}

struct Invoker<C>(C);

impl<C: Callback> Invoker<C> {
    fn call(self, s: String) {
        self.0.call(s)
    }
}

#[test]
fn test() {
    let mut called = false;
    {
        let invoker = Invoker(|s| {
            assert_eq!("expected stuff", s);
            called = true;
        });
        invoker.call("expected stuff".to_string());
    }
    assert!(called);
}

my_crate_method() would then create the Invoker and invoke the callback through it - the testing would then be against Invoker and you take the leap of faith that my_crate_method actually creates and dispatches through one :slight_smile:.