Alex Fischer
Alex Fischer

Reputation: 241

Mocking trait methods that returns Option<&T> causing lifetime conflicts

I'm trying to mock a trait for testing, but ATM not able to implement a function.
I use the crate mockall.

Error msg is:

cannot infer an appropriate lifetime due to conflicting requirements
but, the lifetime must be valid for the static lifetime...
expected `&mut __get_mapping::Expectation<'_>`
   found `&mut __get_mapping::Expectation<'static>`

I'm aware that the problem is, a lifetime conflict. But the item passed to the closure already has an anonymous lifetime, even if I do not state it explicitly.

I tried to find some examples with a solution or hints.... But I could not find a solution.

Is there a way to specify that the returning item has a lifetime other than static? I also tried specifying the lifetime of the return type of the closure, but the error was still the same.

My code so far is:

#[mockall::automock]
pub trait Analyzer {
    //...
    fn get_mapping<'a>(&'a self, old: &Item) -> Option<&'a Item>;
    //...
}



fn test() {
    let mut analyzer = MockAnalyzer::new();
    analyzer.expect_get_mapping().returning(|item:&'_ Item| Some(item));
    // also tried ... returning(|item:&'_ Item| -> Option<&'_ Item> {Some(item)})
    // my initial code was ... returning(|item| Some(item))
    //...
}

Upvotes: 2

Views: 448

Answers (2)

metatoaster
metatoaster

Reputation: 18898

It is possible to have mocks provided by mockall to implement traits that have non 'static lifetimes for the references involved by building the impl manually for the mock that calls into a mocked function that's directly part of the mock. While there isn't any real limitations to the lifetime of the arguments or the return value for the trait to be mocked (note the trait in the example below does not define 'static lifetime for any of the references), references returned by the mock however must generally have a 'static lifetime as per the response given to the GitHub issue asomers/mockall#362. The key is not to use automock but instead use the mock! macro to construct the trait and mock separately. For the first demonstration below, I am assuming Item is something that can easily be made static.

use mockall::mock;

#[derive(Debug, PartialEq)]
pub struct Item(&'static str);

// putting the traits in its own module for clarity later
pub mod traits {
    use crate::Item;

    pub trait Analyzer {
        fn get_mapping<'a>(&'a self, old: &Item) -> Option<&'a Item>;
    }
}

mock! {
    pub Analyzer {}

    // impl the traits::Analyzer for the mock.
    impl crate::traits::Analyzer for Analyzer {
        fn get_mapping<'a>(&'a self, old: &Item) -> Option<&'a Item> {
            // the impl just calls the mocked inner function 
            self.mocked_inner_function(old)
        }
    }
}

Now for the test itself:

use crate::traits::Analyzer as _;
use mockall::predicate::eq;

// the static return value 
static RESULT: Item = Item("result");

#[test]
fn test_analyzer_success() {
    let mut mock_analyzer = MockAnalyzer::new();
    mock_analyzer
        .expect_get_mapping()
        .times(1)
        .with(eq(Item("old")))
        .return_const(Some(&RESULT));

    // test the mock
    assert_eq!(mock_analyzer.get_mapping(&Item("old")), Some(&RESULT));
}

#[test]
fn test_analyzer_failure() {
    let mut mock_analyzer = MockAnalyzer::new();
    mock_analyzer
        .expect_get_mapping()
        .times(2)  // expected to be called twice...
        .with(eq(Item("old")))
        .return_const(Some(&RESULT));

    // ... but only called once.
    assert_eq!(mock_analyzer.get_mapping(&Item("old")), Some(&RESULT));
}

The first test should pass and the second one should fail. Now, if the return value is a more complicated value that can't easily be constructed statically, using OnceLock<Item> defined specifically per each expected test return value may be a viable alternative (another example that returns Option<&String>, that one has the added bonus of mocking a static method):

use std::sync::OnceLock;
static TEST_ANOTHER_TRIAL: OnceLock<Item> = OnceLock::new();

#[test]
fn test_another_trial() {
    TEST_ANOTHER_TRIAL.set(Item("another return value"))
        .expect("this test result value was used elsewhere!");
    let mut mock_analyzer = MockAnalyzer::new();
    mock_analyzer
        .expect_get_mapping()
        .times(1)
        .with(eq(Item("old")))
        .returning(|_| Some(TEST_ANOTHER_TRIAL.get()
            .expect("the return value was not already set")));

    assert_eq!(
        mock_analyzer.get_mapping(&Item("old")),
        Some(&Item("another return value"))
    );
}

While lazy_static! can be used as a legacy alternative to OnceLock (as this was introduce relatively recently in Rust 1.70.0), it is much less idiomatic and can't guarantee that the static value hasn't been reassigned by another test function.

Upvotes: 0

Caesar
Caesar

Reputation: 8484

If your Item is Clone, you might get away with the following:

analyzer
    .expect_get_mapping()
    .returning(|item: &'_ Item| Some(Box::leak(Box::new(item.clone()))));

This does indeed leak heap memory, so if your test code executes this more than a few 10 million times, it may cause trouble.

Upvotes: 1

Related Questions