asomers / mockall

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

Support mockable use of structs without changing the type #479

Closed Ben-PH closed 1 year ago

Ben-PH commented 1 year ago

Related issue: #331

Related discussion: #478

When testing a function with a mocked implementation passed in, it must be a trait by virtue of the fact that a mocked struct is seen as a different type. This is problematic in the context where you do not wish to make the function generic on that input.

I'm not 100% sure what a good solution will look like, but here is a a rough first draft:

// We want to test this function, but we want to mock foo in different ways
fn uses_foo(foo: Foo) -> u32 {
    foo.do_bar()
}

#[test]
fn test_with_mocked_foo() {
    // provides a Foo which can be used in the same way as a `MockFoo` generated from `automock` applied to a `Foo` struct
    let mut mocked_foo = mockall::use_mocked!(Foo::default());

    mocked_foo.expect_do_bar().returning(|| 42);

    let expect_42 = uses_foo(mocked_foo);
}

// this is the current solution:
// T is needed in order to be able to test this function with a mocked: Would prefer to just use a Foo instead of generics
fn uses_foo_trait<T: FooTrait>(foo: T) -> u32 {
    foo.do_bar()
}

#[mockall::automock]
trait FooTrait {
    fn do_bar(&self) -> u32;
}

#[test]
fn test_with_mocked_foo() {
    // provides a Foo which can be used in the same way as a `MockFoo` generated from `automock` applied to a `Foo` struct
    let mut mocked_foo = MockFooTrait::new();

    mocked_foo.expect_do_bar().returning(|| 42);

    let expect_42 = uses_foo_trait(mocked_foo);
}

// How this might be made to work:
mockall::struct_mock! {
    struct Foo {
        bar: Bar,

        // if Foo is being mocked:
        // - adds this field
        // - redefines the structs implementation to always use `self.mock_foo` instead of `self`
        #[mockall::struct_internal]
        mock_foo: MockFoo,
    }

    // Generatios MockFoo similar to `mockall::automock`, and also redefines implementations to use `self.mock_foo`
    #[mockall::automock_struct_internal] // could use `mockall::mock_struct_internal!` instead
    impl Foo {
        fn do_bar(&self) -> u32 {
            self.bar.do_bar()
        }
    }

    // hypothetically would expand to something like....
    impl Foo {
        fn do_bar(&self) -> u32 {
            self.mocked_self.do_bar()
        }

        // and the expansions equivilent to `mockall::automock`
        fn expect_do_bar(...) {
            self.mocked_self.expect_do_bar(...)

        }
    }
}
asomers commented 1 year ago

This is not how Rust works. You cannot create a "Foo" that can be used in the same way as a "MockFoo". Rust is a strongly typed, statically typed language. A variable is either a Foo or a MockFoo, not both. The solution to your problem is to use #[mockall_double] correctly. Or, if for some reason you can't, then you must straighten out all of your cfg directives to ensure that you never pass a MockFoo to a function expecting a Foo or vice versa.

asomers commented 1 year ago

Closing. We discussed the issue in the linked discussion.