asomers / mockall

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

Pass self to return* functions #483

Closed HampusMat closed 1 year ago

HampusMat commented 1 year ago

Greetings. I'm working on a XML library which has a deserializer trait.

The relevant part of the trait looks like this (simplified)

pub trait Deserializer
{
    fn de_tag_with<Output, Err, DeserializeFn>(
        &mut self,
        tag_name: &str,
        deserialize: DeserializeFn,
    ) -> Result<Output, Error<Err>>
    where
        Output: 'static,
        Err: std::error::Error + Send + Sync + 'static,
        DeserializeFn: FnOnce(&TagStart, &mut Self) -> Result<Output, Err> + 'static;
}

I want to be able to mock this function in a neat and tidy way. Like this:

mock_deserializer
    .expect_de_tag_with::<FoodKind, FoodError>()
    .returning(|this, tag_name, func| {
        Ok(func(&TagStart::new(tag_name), this)?)
    })
    .once();

But that's not possible since the closure passed to returning doesn't take self.

Workaround

The workaround i'm using at the moment is the following:

let mock_deserializer = Rc::new(RefCell::new(MockDeserializer::new()));

let deserializer = mock_deserializer.clone();

mock_deserializer
    .borrow_mut()
    .expect_de_tag_with::<String, DeserializerError<Infallible>>()
    .returning_st(move |tag_name, func| {
        Ok(func(&TagStart::new(tag_name), unsafe {
            &mut *deserializer.as_ptr()
        })?)
    })
    .once();

This far from a clean solution however. Using unsafe should not be needed.

asomers commented 1 year ago

The reason that returning methods don't take self is because the closure itself is capable of holding all necessary state. And unlike real methods, the mock object can't have any fields that are useful in a returning closure.

Your case is unusual, though. Your workaround looks like a good one to me. One thing you might do is reconsider whether func needs to receive a reference to this same object, or if a reference to any object would do. If the latter, then you can simply do:

    .returning_st(move |_, _, func| {
        let dummy = MockDeserializer::new();
        Ok(func(&TagStart::new("name"), &dummy)?)
    })
HampusMat commented 1 year ago

It might not need a reference to the same object actually. I don't know how i didn't think of that. It could possibly become a little problematic if one would want the same expectations as the non-dummy mock however. But that's not something i personally plan to do so i'm closing this.

Thanks for the help.