asomers / mockall

A powerful mock object library for Rust
Apache License 2.0
1.45k stars 61 forks source link

Capturing arguments from returning #545

Closed danimihalca closed 7 months ago

danimihalca commented 7 months ago

Hi. I'm a rather new user of your testing framework (and with Rust in general), and I was wondering if there is a way to capture the arguments passed to a mocked function (something similar to Google Mock's SaveArg action if you're familiar). After experimenting with mockall, my first intention was to do it in the returning methods from an Expectation, but I've encountered some lifetime issues.

Below is a small implementation example, a mocked trait which receives a wrapped callback as an argument, which I try to move to the main test scope.

struct Request{}

#[derive(Default)]
struct CallbackWrapper {
    pub callback: Option<Box<dyn Fn()>>
}

#[mockall::automock]
trait Service{
    fn exec(self, request:Request, callback_wrapper:CallbackWrapper);
}

#[cfg(test)]
mod test {

    use super::*;

    #[test]
    fn test() {
        let mut mock_service = MockService::default();

        let mut captured_callback_wrapper =  CallbackWrapper::default();

        mock_service.expect_exec().returning_st(|_request, callback_wrapper| {
            captured_callback_wrapper = callback_wrapper;
            ()
        });

        let request = Request{};
        let arg_callback_wrapper = CallbackWrapper {
            callback: Some(Box::new(||
            {println!("Callback");}))
        };

        mock_service.exec(request, arg_callback_wrapper);

    }
}

The following errors are present where the closure is set for returning_st:

to force the closure to take ownership of `captured_callback_wrapper` (and any other referenced variables), use the `move` keyword: `move `
main.rs(24, 49): original diagnostic
closure may outlive the current function, but it borrows `captured_callback_wrapper`, which is owned by the current function
may outlive borrowed value `captured_callback_wrapper`
main.rs(25, 13): `captured_callback_wrapper` is borrowed here
main.rs(24, 9): function requires argument type to outlive `'static`
main.rs(24, 49): to force the closure to take ownership of `captured_callback_wrapper` (and any other referenced variables), use the `move` keyword: `move `

Am I using the API wrong, or for some reason, it expects that whatever is captured in returning_st's closure to have a static lifetime?

asomers commented 7 months ago

It's a lifetime problem. The argument of returning_st does indeed need to be static, for complicated reasons. But that's ok, if you do it like this:

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

        let mut captured_callback_wrapper =  Arc::new(Mutex::new(CallbackWrapper::default()));
        let ccw2 = captured_callback_wrapper.clone();

        mock_service.expect_exec().returning_st(move |_request, callback_wrapper| {
            *ccw2.lock().unwrap() = callback_wrapper;
            ()
        });
danimihalca commented 7 months ago

Understood. Thanks for the clarification and solution!