asomers / mockall

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

Expectations aren't verified if mock object never drops #396

Closed DE-mohammad closed 2 years ago

DE-mohammad commented 2 years ago

I'm having an issue with mocking external traits, and it seems that if, when an external trait is mocked with a struct, and the new generated mock struct is accessed with a mutable reference in order to set expectations, then a test passes incorrectly in certain specific circumstances. The reason I needed to do this is that my application requires a function call to take an instance of the (mocked) struct, and that struct is bound by a static lifetime. In the examples below, I use [lazy_static]() to create a global mutable singleton on a Mutex lock, or I use Box::leak() to create a variable inside a function with a static lifetime.


Here is an example of this issue occurring with lazy_static:

use mockall::*;
use mockall::predicate::*;

// NOTE: this is the example "external" trait and would normally be in an external file foo.rs
mod foo {
    pub trait Foo {
        fn foo(&self, x: u32);
    }
}

fn main() {}

#[cfg(test)]
mock!{
    MyStruct {}
    impl crate::foo::Foo for MyStruct {
        fn foo(&self, x: u32) {}
    }
}

#[cfg(test)]
mod tests {
    use lazy_static::lazy_static;
    use std::sync::Mutex;
    use super::*;

    lazy_static!{
        static ref MOCK_MY_STRUCT: Mutex<MockMyStruct> = Mutex::new(MockMyStruct::new());
    }

    #[test]
    fn test_static_mock() {
        MOCK_MY_STRUCT.lock().unwrap()
            .expect_foo()
            .times(1)
            .return_const(());

        // test should fail but PASSES INCORRECTLY even though function was never called
    }

    #[test]
    fn test_local_mock() {
        let mut mock = MockMyStruct::new();
        mock.expect_foo()
            .times(1)
            .return_const(());

        // test fails CORRECTLY as expected because function was never called
    }
}


Here is another example using std::Box::leak instead:

use mockall::*;
use mockall::predicate::*;

// NOTE: this is the example "external" trait and would normally be in an external file foo.rs
mod foo {
    pub trait Foo {
        fn foo(&self, x: u32);
    }
}

fn main() {}

#[cfg(test)]
mock!{
    MyStruct {}
    impl crate::foo::Foo for MyStruct {
        fn foo(&self, x: u32) {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::boxed::Box;

    #[test]
    fn test_static_mock() {
        let mock: &mut MockMyStruct = Box::leak(Box::new(MockMyStruct::new()));
        mock.expect_foo()
            .times(1)
            .return_const(());

        // test should fail but PASSES INCORRECTLY even though function was never called
    }

    #[test]
    fn test_local_mock() {
        let mut mock = MockMyStruct::new();
        mock.expect_foo()
            .times(1)
            .return_const(());

        // test fails CORRECTLY as expected because function was never called
    }
}

In both examples, the test test_static_mock() passes incorrectly as per this output.

running 2 tests
test tests::test_static_mock ... ok
test tests::test_local_mock ... FAILED

failures:

---- tests::test_local_mock stdout ----
thread 'tests::test_local_mock' panicked at 'MockMyStruct::foo: Expectation(<anything>) called 0 time(s) which is fewer than expected 1', src\main.rs:14:1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    tests::test_local_mock

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

EDIT: typo, test_static_mock() PASSES incorrectly

asomers commented 2 years ago

Your problem is that the mock object never drops. Normally, if a mock object is expected to be called but never gets called, that will trigger a panic when the mock object drops. But in both of these cases, the mock object never drops. So while by the end of your test function the expectation hasn't been satisfied yet, it conceivably might be satisfied in the future. I don't think there's any way for Mockall to provide the behavior you want, because Mockall doesn't know when your test function ends.

But there's an easy solution! Just call the mock object's checkpoint method at the end of your test, like this:

    #[test]
    fn test_static_mock() {
        MOCK_MY_STRUCT.lock().unwrap()
            .expect_foo()
            .times(1)
            .return_const(());

        MOCK_MY_STRUCT.lock().unwrap().checkpoint();
    }
DE-mohammad commented 2 years ago

Thank you for the quick response. That makes sense and clarifies things quite a bit, and it solved my issue perfectly.