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 lifetimeerror[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 traiterror[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 traiterror: aborting due to 2 previous errors
There are two problems here:
- 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 theasync_trait
macro. - The harder problem is that the
foo
method needs to return a value with a lifetime that has no relationship to any offoo
's arguments (includingself
for non-static methods). Yet Mockall needs to create and store itsExpectation
somewhere. Ultimately there must be a reference to thatExpectation
from eitherself
or from a static location (for static methods). The only way that can work is if theExpectation
is'static
. That's not necessarily a fatal problem. Plenty ofFuture
s do have'static
values, for examplefuture::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:
- The
async_trait
macro creates an additional, undocumentedlife0
lifetime parameter - 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 befn sync_publish_request(&self, url: Uri) -> futures::BoxFuture<'static, Result<Timetoken, MockTransportError>> {}
- it's possbile to use
cargo expand
to implement theimpl Transport for MockTransport { ... }
withoutasync_trait
and make it more optimized (skip the internalasync
wrapper) - probably with the previous trick you just define the
fn publish_request
howasync_trait
does it right at themock!
; 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.