asomers / mockall

A powerful mock object library for Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Doesn't work with async traits

krojew opened this issue · comments

When using async traits (from the async-trait crate) both automock and mock! fail to generate valid code.

Can you give an example please?

use async_trait::async_trait;
use mockall::*;

#[async_trait]
#[automock]
trait Widget {
    async fn foo();
}

error[E0261]: use of undeclared lifetime name 'async_trait
--> src\main.rs:5:1
|
5 | #[automock]
| ^^^^^^^^^^^ undeclared lifetime

error[E0582]: binding for associated type Output references lifetime 'async_trait, which does not appear in the trait input types
--> src\main.rs:5:1
|
5 | #[automock]
| ^^^^^^^^^^^

error: aborting due to 2 previous errors

Switching the order:

#[automock]
#[async_trait]
trait Widget {
    async fn foo();
}

error[E0706]: trait fns cannot be declared async
--> src\main.rs:4:11
|
4 | #[automock]
| ^

error[E0706]: trait fns cannot be declared async
--> src\main.rs:4:11
|
4 | #[automock]
| ^

error[E0195]: lifetime parameters or bounds on method foo do not match the trait declaration
--> src\main.rs:4:1
|
4 | / #[automock]
5 | | #[async_trait]
| | --------------
| | |
| | lifetimes in impl do not match this method in trait
6 | | trait Widget {
7 | | async fn foo();
| |________________^ lifetimes do not match method in trait

error[E0195]: lifetime parameters or bounds on method foo do not match the trait declaration
--> src\main.rs:4:1
|
4 | / #[automock]
5 | | #[async_trait]
| | --------------
| | |
| | lifetimes in impl do not match this method in trait
6 | | trait Widget {
7 | | async fn foo();
| |________________^ lifetimes do not match method in trait

error: aborting due to 2 previous errors

There are two problems here:

  1. The async fn syntax is unstable, and I'm not sure I should invest much effort in supporting unstable syntax. But that's the easy problem; you can work around it simply by expanding the async_trait macro.
  2. The harder problem is that the foo method needs to return a value with a lifetime that has no relationship to any of foo's arguments (including self for non-static methods). Yet Mockall needs to create and store its Expectation somewhere. Ultimately there must be a reference to that Expectation from either self or from a static location (for static methods). The only way that can work is if the Expectation is 'static. That's not necessarily a fatal problem. Plenty of Futures do have 'static values, for example future::ready(42). But if your expectation generates that future from a closure, then the closure must also be 'static. That precludes it from capturing any references, at least not safely.

This all may be possible. If you want to help, try to come up with a realistic example, including something like a Future that returns a reference.

I don't quite understand what you mean by a realistic example. The only thing I forgot is &self in foo(), if that is the problem.

A realistic example would include setting expectations and calling the function from an actual async block.

A natural assumption would be to return the future output, rather than the future itself.

A natural assumption would be to return the future output, rather than the future itself.

You mean when setting the expectation? No. I think what you're getting it is that you'd like to return something like 42u32 instead of future::ready(42u32). But for Mockall to accept that would be penny-wise and pound-foolish, because then it wouldn't be possible to return a future that isn't ready yet.

That is true, there might be such use cases.

That is true, there might be such use cases.

Indeed, I've run into them myself.

Ok, until the async fn syntax is stable, here's your workaround. You must use mock! instead of #[automock]. And the result's lifetime must be 'static in test mode (I'll tackle that part in issue #76 )

#[async_trait]
trait Widget {
    async fn foo(&self);
}
mock! {
    Widget {
        fn foo(&self) -> Pin<Box<Future<Output = ()> + Send + 'static>>;
    }
}

#[test] 
fn my_test() {
    let mut widget = MockFoo::default();
    widget.expect_foo()
        .returning(|| Box::pin(future::ready(())));
    block_on(widget.foo());
}

One additional comment - you can't use self: Arc<Self> when using mock! this way.

It seems like the workaround no longer works. Can someone update the example for the current mockall version?

I'm stuck with this:

#[async_trait]
pub trait Transport: Clone + Send + Sync {
    type Error: std::error::Error + Send + Sync;
    async fn publish_request(&self, url: Uri) -> Result<Timetoken, Self::Error>;
    ...
}

mock! {
    pub Transport {}
    trait Clone {
        fn clone(&self) -> Self;
    }
    trait Transport {
        type Error = Error;
        fn publish_request(&self, url: Uri) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Timetoken, Error>> + Send + 'static>> {}
        ...
    }
}
  --> pubnub-core/src/tests/mock/transport.rs:11:1
   |
11 | / mock! {
12 | |     pub Transport {}
13 | |     trait Clone {
14 | |         fn clone(&self) -> Self;
...  |
18 | |         fn publish_request(&self, url: Uri) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Timetoken, Error>> + Send + 'static>> {}
   | |____________________________^ lifetimes do not match method in trait

It'd be great if I could just implement those manually somehow

@MOZGIII I see the problem, partly. For now the only workaround I can recommend would be to eschew the async_trait macro and implement it manually instead, like this:

use futures::future::ready;
use mockall::mock;
use std::pin::Pin;
use std::error::Error;
use std::io;
use std::future::Future;

pub trait Transport: Clone + Send + Sync {
    type Error: Error + Send + Sync;
    fn publish_request<'async_trait>(
        &self,
        url: u32
    ) -> Pin<Box<dyn Future<Output = Result<u32, Self::Error>> + Send + 'async_trait>>;
}

mock! {
    pub Transport {}
    trait Clone {
        fn clone(&self) -> Self;
    }
    trait Transport {
        type Error = io::Error;
        fn publish_request<'async_trait>(
            &self,
            url: u32
        ) -> Pin<Box<dyn Future<Output = Result<u32, io::Error>> + Send + 'async_trait>> {}
    }
}

#[test]
fn t() {
    let mut mock = MockTransport::new();
    mock.expect_publish_request()
        .returning(|_| Box::pin(ready(Ok(42u32))));
    mock.publish_request(0);
}

The problem is caused by:

  1. The async_trait macro creates an additional, undocumented life0 lifetime parameter
  2. Mockall yet doesn't know how to handle lifetime parameters that are used by neither the arguments nor the return values. I'll open a new issue for that.

I found a different workaround:

mock! {
    pub Transport {
        fn sync_publish_request(&self, url: Uri) -> Result<Timetoken, MockTransportError> {}
        ...
    }
    trait Clone {
        fn clone(&self) -> Self;
    }
}

// We implement the mock manually cause `mockall` doesn't support `async_trait` yet.
#[async_trait]
impl Transport for MockTransport {
    type Error = MockTransportError;
    async fn publish_request(&self, url: Uri) -> Result<Timetoken, Self::Error> {
        self.sync_publish_request(url)
    }
    ...
}

I'm using the same workaround as @MOZGIII and it seems to be working fine, although you loose fine control over returning Future results.

If you ended up on this thread, like me, searching how to use mockall with async traits. I've written a small, but complete, example based on the above workaround from @MOZGIII. You can find it here https://github.com/mibes/mockall-async

I have three more tips:

  • fn sync_publish_request(&self, url: Uri) -> Result<Timetoken, MockTransportError> {} and be fn sync_publish_request(&self, url: Uri) -> futures::BoxFuture<'static, Result<Timetoken, MockTransportError>> {}
  • it's possbile to use cargo expand to implement the impl Transport for MockTransport { ... } without async_trait and make it more optimized (skip the internal async wrapper)
  • probably with the previous trick you just define the fn publish_request how async_trait does it right at the mock!; I havent tried that yet though.

BTW instead of using cargo expand, you can set the environment variable MOCKALL_DEBUG . That will produce better output.

BTW I want everybody to know that I haven't forgotten about this issue. I do intend to implement it! But Mockall's internals had gotten clumsy, because of several rewrites in 2019. I had to change my whole approach several times. Now, I'm working on doing it again. The new branch is only a few test cases away from passing, and after that I'll get back to new features.
https://github.com/asomers/mockall/compare/2020_refactor

Party's on! I suspect there are many async_trait edge cases that I didn't think of. So please try this feature out and don't hesitate to open new issues.