How to pass closure for mocking async call?

So that I can unit test an async call that's expensive, how can I pass the function so that my unit tests can mock it?

Here's what I have nearly working, except the argument reference causes lifetime issues I am beyond confused with:

Permalink to the playground

You can't describe the future type separately from the function type, because the future borrows from the reference given to the function, so there isn't just a single future type involved, but a family of them.

You have to use the AsyncFn trait instead of fn(), which allows you to avoid mentioning the future type as a single type parameter:

async fn process<F>(
    request: Request,
    fetch_config_fn: F,
) -> Result<(), Error>
where
    F: AsyncFn(&Request) -> Result<Config, Error>,

However, that can't yet express that the future needs to be Send + Sync. If you don't require that, then this is all you need; if you do, use the both-less-and-more-powerful async_fn_traits trait library. It probably shouldn't be necessary in this case, though — that's more of a problem for Box<dyn AsyncFn> scenarios than non-type-erased generic async functions.

2 Likes

I look at Rust in 2 different ways:

  • How can anyone (even AI) remember all of these nuances?!
  • But wait... Do I really trust the abstractions other languages use to hide these nuances?

Eventually it's the ladder is always the thought I'm agreeing with

There's almost always more complexity than you see on the surface, in any part of computing, not just Rust, and not just programming languages. With practice, it becomes easier to remember (or look up when needed) all the pieces you need, particularly because more of them are like other things you have met before.

1 Like

By using injectorpp, you don't need to pass a function fetch_config to mock process. Maybe this can simplify your code:

use injectorpp::interface::injector::*;

struct Request {}
struct Config {}

#[derive(Debug)]
struct Error {}

async fn process(
    _request: Request,
) -> Result<(), Error> {
    // Process the request
    // Could return any result, here we just return Ok
    Ok(())
}

#[tokio::test]
async fn test_async() {
    let mut injector = InjectorPP::new();
    injector
        .when_called_async(injectorpp::async_func!(process(
            Request{}
        )))
        .will_return_async(injectorpp::async_return!(Err(Error {}), Result<(), Error>));

    let request = Request {};
    let result = process(request).await;
    assert!(result.is_err());
}