asomers / mockall

A powerful mock object library for Rust
Apache License 2.0
1.5k stars 62 forks source link

Mocking a function taking an FnMut closure incompatible with concretize #577

Closed vlovich closed 2 months ago

vlovich commented 5 months ago

Tried on Stackoverflow but I think this won't get the right eyes there. I have a trait I'm trying to mock that looks like below. It has a for_each method that takes an FnMut and invokes it on all items of interest. UnderTest is the thing I'm trying to test where I give it a mocked Foo instance and it calls for_each and does something with each element. The problem is that when I use concretize, the returning expectation receives a &dyn FnMut instead of &mut dyn FnMut meaning I can't invoke the callback to invoke the closure do_something provides. If I get rid of #[concretize] & make F: 'static for for_each, then I get an invokable function, but then do_something won't compile because &mut sum doesn't have static lifetime (i.e. the static lifetime is purely for mocking & adds requirements I don't need when I don't mock).

Is there a better way?

#[cfg_attr(test, automock)]
trait Foo {
    #[cfg_attr(test, concretize)]
    fn for_each<F>(&self, processor: F)
    where
        Self: Sized,
        F: FnMut(&u32);
}

#[derive(Default)]
struct UnderTest;

impl UnderTest {
   fn do_something<F: Foo>(&self, foo: &F) {
     let mut sum = 0;
     let sum_mut = &mut sum;
     foo.for_each(|x| {
         *sum_mut += *x;
     })
     eprintln!("Sum of all numbers is {sum}");
   }
}

#[cfg(test)]
#[test]
fn do_something() {
   let mut foo = MockFoo::new();
   foo.expect_for_each().times(1).returning(|cb| {
     cb(&0);
     cb(&1);
   });

   let t = UnderTest::default();
   t.do_something(&foo);
}
asomers commented 5 months ago

I think you've found a bug. I'll look into it.

asomers commented 3 months ago

BTW, I want to let you know that I haven't forgotten about this. It turned out to be harder than I thought, but I have a branch that almost works.