asomers / mockall

A powerful mock object library for Rust
Apache License 2.0
1.45k stars 61 forks source link

Mockall Double of struct in external crate #563

Closed gregdavisfromnj closed 5 months ago

gregdavisfromnj commented 5 months ago

Hello, I have some test code that directly mocks structs in the MongoDB crate. In my case, I have references to the mocked struct at two layers, architecturally speaking, and so the Rust compiler needs to deal with mock type usage declarations in two modules. I have the runtime and test usage of mocks and non-mocks working with Mockall, but I ultimately have resorted to the following use statements, which I see is not one of the test cases in the mockall_double crate:

#[cfg(not(test))]
use mongodb::Client;
#[cfg(test)]
use crate::mocks::mongodb::MockClient as Client;

...where MockClient is defined by a mock! macro in a module that is internal to my codebase (mocks::mongodb).

I don't see a way to get the mockall_double::double macro to generate my example block above, without creating an optional parameter for the attribute macro that takes the type name to use for the test configuration instead of assuming that the type name should be macro input type name prefix with "Mock". Is there a way of organizing my mocks into submodules that does not require redefinition of all other structs, etc, in the external crate that are not mocked and so that they are still accessible in the outer layer (and thus allowing me to use mockall_double::double as-is)?

Or, would you be open to a PR that adds a parameter to the double macro so it can generate the block above from something like this:

#[mockall_double::double(crate::mocks::mongodb::MockClient)]
use mongodb::Client;

Getting the declaration of the "double" down to one use statement instead of two conditional statements would make for less lines of code, of course, but also prevents situations where my IDE reorganizes the use statements and splits them up in a way that makes it non-obvious that they are closely/semantically related.

asomers commented 5 months ago

Sure there is. All you need to do is to export Client and MockClient from the same module. Perhaps like this:

mod mongodb {
    pub use ::mongodb::Client;
    #[cfg(test)]
    mock! {
        pub Client 
        ...
     }
}
#[double]
use mongodb::Client
gregdavisfromnj commented 5 months ago

Ah, okay... this was a bit of Rust I was not aware of being able to do with modules and redeclaring things that are really defined in another crate. My case is trickier though, and so to use another random mongodb type that is not mocked, for example ClientOptions below, I had to make the declaration like this to be able to resolve it:

use mongodb::options::ClientOptions;  // import to satisfy usage in main function

mod mongodb {
    pub use ::mongodb::Client;
    pub mod options {
        pub use ::mongodb::options::ClientOptions;
    }
    #[cfg(test)]  // import to satisfy usage in mock macro
    use mongodb::options::ClientOptions;
    #[cfg(test)]
    mock! {
        pub Client {
        ...
        pub fn with_options(options: ClientOptions) -> mongodb::error::Result<Self>;
        }
     }
}

#[double]
use mongodb::Client

fn main() {
    ...
    /* create a ClientOptions */
    /* create Client from with_options, passing it the ClientOptions */
    /* pass the Client to a cool_service::CoolService */ 
    ...
}

Now suppose the CoolService is in another file named cool_service.rs. The use statement to import mongodb::Client in that file, using the double macro, seems to require the crate:: prefix to make sure the MockClient is found in a different file for test build configuration. Like this:

// cool_service.rs

#[double]
use crate::mongodb::Client;

Going back to the hypothetical first file where main is defined, I can seem to use either use crate::mongodb::Client; or use mongodb::Client; for the import because they seem to mean the same thing there.

And finally, in case anyone else ever goes down this path like me... I was able to move the contents of the mongodb module defined above to its own module as a file name mongodb.rs, and then replace the module definition with a declaration of the module like mod mongodb;. Because MockClient is actually used in that file during the test configuration, and not mongodb::Client, I needed to annotate the pub use ::mongodb::Client; line with #[cfg(not(test))] to avoid an unused-import warning during normal build.

Thanks for the quick response!