mockall
mockall copied to clipboard
Allow expectation on async trait function to return a future
I saw in the changelog that you implemented support for async_trait. Thank you!
I noticed a limitation in this implementation. In the expectation we have to return a non future value. This prevents some advanced mocking from working:
#[mockall::automock]
#[async_trait::async_trait]
trait T {
async fn foo(&self) -> u32;
}
fn main() {
let mut t = MockT::new();
t.expect_foo().returning(|| 1);
// These two don't work:
t.expect_foo().returning(|| async { 1 });
t.expect_foo()
.returning(|| async { futures::future::pending().await });
}
This is an observable difference because async functions can either return their value or be pending. By only being able to return the values we cannot mock and test pending behavior. In the async { 1 }
you can imagine that we want to be pending first for a short time (like sleeping, or waiting for a signal) and only then return the value. Or we get the value from an async channel.
These types of tests are probably rare and the current functionality is correct for most users but it might be nice to have. From an api perspective the most general solution is to have expect_foo
always return a future so you would need to start an async block in the closure. I understand this is a little more work for users that don't need this but it is more correct.
Alternatively there could be another postfix like returning_async
but I'm not sure how this meshes with the existing _st
.
Or maybe mockall could detect whether returning
on an async trait function expectation returns a normal value or a future. I don't know whether this is possible.
If you know that you will need set set expectations that aren't immediately ready, write the method as a normal method returning a future rather than an async method. Can you use cargo expand
to help understand what the async_trait
is doing, so you'll know exactly how to write the function's signature.
mock! {
T {
async fn always_immediately_ready(&self) -> i32;
fn might_not_be_ready(&self) -> Future<i32>;
}
}
I see, I can keep using async fn
in the trait (this is important to me) and then use mock!
with the different return type instead of automock
. In my example this looks like this
#[async_trait::async_trait]
trait T {
async fn foo(&self) -> u32;
}
mockall::mock! {
T {
fn foo(&self) -> impl std::future::Future<Output = u32>;
}
}
fn main() {
let mut t = MockT::new();
t.expect_foo().returning(|| Box::new(async { 1 }));
}
This works. Thank you! If you would like I could make a PR to add an example like this to the docs.
Glad it works! I think better docs would help here. However, I'd like to see a more realistic use first. Fortunately, I expect to soon have the good fortune of using Mockall with an async trait myself, soon. But it might be a few more weeks before I get to it.
The realistic use I have is that I want to test the behavior of a piece of code that has multiple futures running at the same time (calling select
on them). In order to test this I need the futures to sometimes be pending. The futures come calls to a mockable trait.
You might also just try to change #[automock]
and #[async_trait]
order, so #[async_trait]
expands first.
#[async_trait::async_trait]
#[mockall::automock]
trait T {
async fn foo(&self) -> u32;
}
Just to give an example of another use case: during testing, it could be useful to delay a response to trigger a timeout that must be handled correctly. And I want to test this behaviour.
I've also recently came across the problem of testing timeouts in particular. While I found a workaround for my particular case by triggering some retry logic in the application itself, this could definitely be useful.
Now that Rust 1.75 is released, the same limitations exist with the native implementation of async functions in traits, plus an additional one.
You can return a synchronous value when following the happy path:
use mockall::{*, predicate::*};
#[automock]
trait Example {
async fn demo(&self) -> i32;
}
async fn usage(x: impl Example) -> i32 {
x.demo().await + 1
}
#[tokio::main]
async fn main() {
let mut mock = MockExample::new();
mock.expect_demo().times(1).returning(|| 42);
assert_eq!(43, usage(mock).await);
}
You can return a Pin<Box<dyn Future>>
by changing your trait declaration to return impl Future
:
use mockall::{predicate::*, *};
use std::future::Future;
#[automock]
trait Example {
fn demo(&self) -> impl Future<Output = i32>;
}
async fn usage(x: impl Example) -> i32 {
x.demo().await + 1
}
#[tokio::main]
async fn main() {
let mut mock = MockExample::new();
mock.expect_demo()
.times(1)
.returning(|| Box::pin(async { 42 }));
assert_eq!(43, usage(mock).await);
}
However, you cannot have the trait method be async
(for now?) and use the mock!
macro to return impl Trait
:
use mockall::{predicate::*, *};
use std::future::Future;
trait Example {
async fn demo(&self) -> i32;
}
mock! {
Example {}
impl Example for Example {
fn demo(&self) -> impl Future<Output = i32>;
}
}
async fn usage(x: impl Example) -> i32 {
x.demo().await + 1
}
#[tokio::main]
async fn main() {
let mut mock = MockExample::new();
mock.expect_demo()
.times(1)
.returning(|| Box::pin(async { 42 }));
assert_eq!(43, usage(mock).await);
}
error: method `demo` should be async because the method from the trait is async
--> src/main.rs:11:1
|
6 | async fn demo(&self) -> i32;
| ---------------------------- required because the trait method is async
...
11 | / mock! {
12 | | Example {}
13 | |
14 | | impl Example for Example {
15 | | fn demo(&self) -> impl Future<Output = i32>;
16 | | }
17 | | }
| |_^
|
= note: this error originates in the macro `mock` (in Nightly builds, run with -Z macro-backtrace for more info)
In addition to all that, it'd be nice if mockall performed the pin-boxing for us. That is, if returning
was defined something like
fn returning(&mut self, v: impl Future) {
use_this_somehow(Box::pin(v))
}
Is there any movement on this? I would love a solution for my use case
I see, I can keep using
async fn
in the trait (this is important to me) and then usemock!
with the different return type instead ofautomock
. In my example this looks like this#[async_trait::async_trait] trait T { async fn foo(&self) -> u32; } mockall::mock! { T { fn foo(&self) -> impl std::future::Future<Output = u32>; } } fn main() { let mut t = MockT::new(); t.expect_foo().returning(|| Box::new(async { 1 })); }
This works. Thank you! If you would like I could make a PR to add an example like this to the docs.
Adding to this, this will also work in Rust 1.75 without the need for async_trait
. Here's my implementation in case anyone new swings by this issue and needs a fix. It uses a combination of some of the solutions on this page as well as the recommended practices in the Rust Blog entry on async traits
#[trait_variant::make(MyTrait: Send)]
#[cfg_attr(test, automock)]
pub trait LocalMyTrait {
async fn trait_method(
&mut self,
arg_1: String,
arg_2: u8,
) -> anyhow::Result<()>;
}
// Actual struct
#[derive(Clone, Debug)]
pub struct MyStruct {
member_1: u64,
}
// Actual trait impl for struct.
impl MyTrait for MyStruct {
async fn trait_method(
&mut self,
arg_1: String,
arg_2: u8,
) -> anyhow::Result<()> {
// Do some async stuff here
Ok(())
}
}
#[cfg(test)]
mock! {
pub MyStruct {}
impl Clone for MyStruct {
fn clone(&self) -> Self;
}
impl MyTrait for MyStruct {
// This implementation of the mock trait method is required to allow the mock methods to return a future.
fn trait_method(
&mut self,
arg_1: String,
arg_2: u8,
)-> impl std::future::Future<Output=anyhow::Result<()>> + Send;
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn test_mock_trait_method_can_sleep() {
let mut mock_struct = MyStruct::default();
mock_struct
.expect_trait_method()
.times(1)
.returning(|arg_1, arg_2| {
// Any access to args must be done before async block because the lifetime of the async block could be longer than the lifetime of the borrowed arguments.
Box::pin(async {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok(())
})
});
mock_struct.trait_method("hello_world".to_string(), 5).await.unwrap();
}
}
Apologies if there are any syntax errors, I wrote this in the github comment editor 😆