Mock API difficulties

There seams to be a little bit of difficulty for mocking libraries to be able to write an api that does not require you manually type the method declarations that you are trying to mock when those methods are defined in an external crate.

For example, in the mockall library, if you have...

struct Foo {
    some_var: SomeType
}
impl Foo {
    fn bar(&self) -> f64{
        ...
    }
}

defined in an external crate, you would mock it by...

mock! {
    Foo {
        bar(&self)
    }
} 

It would be nice if you could instead do the following...

#[mockable]
use some_crate::Foo;

In you source code and

use super::*;

#[test]
fn some_test() {
    let mut mock = MockFoo::new();
    mock.bar_return(3.14);

}

In your test.

But this is not possible given the macro can only see the use some_crate::Foo token stream, not the actual declaration or impl blocks of Foo. Does anyone have suggestions to how a library could be designed such that we could eliminate this boilerplate code? Is there any feature that may help that I am missing here? Perhaps an RFC could be made to add a keyword to request the token stream for use some_crate::Foo to be imported before the macro call.

Have you looked at faux as an alternative to mockall? It is currently under active development. Uses procedural macros on the mocked type in place of duplicating mock interfaces.

I haven't used faux myself, but it's another crate I'm keeping a close eye on.

Funny you should mention it. I had a conversation with the main developer last night. Faux currently cannot mock things defined in an external crate with minimum boilerplate due to the issue stated previously. We both agreed that mocking something defined within an external crate with minimum boilerplate would be much more feasible if the contents inside of the macro could be known/expanded before the macro itself.

or... perhaps there is another way that we have not considered.

This is already built into the language and doesn't need any extra RFCs or language support, it's called a "trait". For languages with static typing and the concept of an interface, I've found explicit mocking frameworks to be largely unnecessary.

It's already good practice to use interfaces to create seams between components (to reduce coupling, etc.), so "mocking" is just a case of creating some dummy struct which implements the interface and you can inspect its properties afterwards to make sure things happened as expected. If an interface is kept thin and succinct then this becomes trivial.

If you find it difficult to create a succinct interface (e.g. because your code depends on the type having a dozen different methods and behaviors) then I'd interpret that as the code telling you your components are overly coupled, and will lead to unnecessary maintenance burden. That's usually the point where I take a step back, look at my design, and figure out how I can separate it into clean layers or extract behaviour into interfaces.

2 Likes

Hi @Michael-F-Bryan. I personally have found it odd to use a trait for mocking for a few reasons.

  1. traits are intended to define shared behavior between two structs. If the trait is not defining share behavior but existing only so an item could be mocked manually then I think the trait is negatively affecting the design of the code.

  2. Its common the thing you want to mock is defined in an external library and not a trait.

Perhaps I have not found a great example of mocking manually with structs. I am open if you would like to point me to a repo that you think does this well.

In general though, I think mocking libraries could do a lot more if a macro existed that allowed you to expose a module, struct, or constant in a given library. For example if...

expose!{use std::SomeStruct};

expanded to the struct definition and all of the implementation blocks such as...

struct Foo {
    u32 a;
    u32 b;
}

impl Foo {
    fn bar(&mut self)
}

Then mocking libraries could do a lot more to write macros that would make mocking anything much more seamless.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.