Mockall中FnMut参数以&而非&mut传递无法调用的问题求助
Mock FnMut 参数时的编译错误
问题场景
定义了包含for_each方法的trait Foo,使用mockall的automock和concretize宏生成Mock,测试中尝试Mockfor_each方法并调用传入的闭包时,出现编译错误:Mock生成的参数是不可变引用的FnMut trait对象,无法调用(因为FnMut需要可变引用)。
定义的Trait与测试目标代码
#[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) { foo.for_each(|x| { eprintln!("Invoked with {x}"); }) } }
测试代码
#[cfg(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); }
编译错误信息
expected closure signature `for<'a> fn(&'a (dyn for<'a> std::ops::FnMut(&'a u32))) -> _` found closure signature `for<'a> fn(&'a mut (dyn for<'a> std::ops::FnMut(&'a u32))) -> _`
原因分析
问题出在mockall的concretize宏处理逻辑:for_each接收的是F: FnMut(&u32)类型的参数,concretize会将这个泛型参数抽象为trait对象,但默认包装成了不可变引用&dyn FnMut(...)。而FnMut闭包的调用会修改其内部状态,必须通过可变引用才能调用,因此直接调用不可变引用的闭包会触发编译错误。
解决方案
方案1:修改Trait方法的参数传递方式(推荐)
调整trait的for_each方法,让闭包以可变引用的形式传递,这样mockall生成的Mock参数类型会匹配FnMut的调用要求:
#[cfg_attr(test, automock)] trait Foo { #[cfg_attr(test, concretize)] fn for_each<F>(&self, processor: &mut F) where Self: Sized, F: FnMut(&u32); }
同时修改UnderTest中的调用代码:
impl UnderTest { fn do_something<F: Foo>(&self, foo: &F) { let mut cb = |x| { eprintln!("Invoked with {x}"); }; foo.for_each(&mut cb); } }
测试代码无需修改,此时returning闭包接收的是&mut dyn FnMut(...),可以正常调用闭包。
方案2:移除concretize宏,显式指定泛型参数
如果不想修改原trait的签名,可以去掉#[cfg_attr(test, concretize)],然后在测试中显式指定for_each的泛型参数类型:
// 修改后的Trait定义 #[cfg_attr(test, automock)] trait Foo { fn for_each<F>(&self, processor: F) where Self: Sized, F: FnMut(&u32); }
测试代码调整为:
#[cfg(test)] fn do_something() { let mut foo = MockFoo::new(); // 显式指定泛型参数为Box包裹的FnMut trait对象 type TestCallback = Box<dyn FnMut(&u32)>; foo.expect_for_each::<TestCallback>().times(1).returning(|mut cb| { cb(&0); cb(&1); }); let t = UnderTest::default(); t.do_something(&foo); }
这种方式避免了修改原代码,但需要显式指定泛型参数,灵活性稍差。
内容的提问来源于stack exchange,提问作者Vitali
相关产品推荐
相关产品推荐

