Alex Kreidler

ProjectsBooksBlog

Async Tests in Rust

Jun 24, 2020

Note: this was written when Async had just recently landed in stable rust and the library ecosystem for it was still settling down.

TLDR: Use tokio’s #[tokio::test] macro to write quick and easy tests in Rust using async features.

Backstory

I looked all over the internet and couldn’t find a nice way to test those pieces of code that are async functions in Rust.

BTW if you haven’t read What Color is Your Function?, do that now.

Attempts

So here’s what I did.

1. futures-await-test

I looked up “async tests rust” and found this crate: https://github.com/ngg/futures-await-test

I copied their test.rs file and it worked fine. However, when I tried running it on my code, it failed with a 'not currently running on the Tokio runtime.' error.

#[cfg(test)]
mod tests {
    use crate::plugin::*;
    use super::*;
    use crate::plugin::Requestor;
    // use futures;
    use futures_await_test::async_test;

    #[async_test]
    async fn get() -> AResult<()> {
        let mut r = super::Requestor{};
        r.configure(Config{version: "".to_string()});
        // futures::executor::block_on
        let res = r.make_request("https://google.com").await;
        println!("{:#?}", res?);
        Ok(())
    }
}

I installed cargo-expand and looked at the code that is generated from the macro. I couldn’t see anything egregious, so I tried something else.

2. futures::executor::block_on

I went lower level, and wrote a regular #[test] fn, but used futures and block_on. Unfortunately, I got the exact same error.

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugin::Requestor;
    use crate::plugin::*;
    use futures;

    #[test]
    fn get() -> AResult<()> {
        let mut r = super::Requestor {};
        r.configure(Config {
            version: "".to_string(),
        })?;
        futures::executor::block_on(async {
            let res = r.make_request("https://google.com").await;
            match res {
                Ok(r) => println!("{:#?}", r),
                Err(e) => panic!(e),
            }
        });
        Ok(())
    }
}

3. tokio

Maybe this process illustrates my relative foolishness with Rust so far, and my finally understanding the need for an async runtime. I guess I just thought futures did that automatically.

I am curious if I could use a tokio runtime but a regular fn like above.

The sad thing is that the #[tokio::test] macro is so poorly documented and hidden that it was the last thing I tried.

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugin::Requestor;
    use crate::plugin::*;

    #[tokio::test]
    async fn get() -> AResult<()> {
        let mut r = super::Requestor{};
        r.configure(Config {
            version: "".to_string(),
        })?;
        
        let url = "https://google.com";
        println!("calling {}", url);
        let res = r.make_request(url).await?;
        println!("{:#?}", res);
        Ok(())
    }
}

Anyways, lesson learned, and a few Rust debugging skills were luckily picked up on the way:

  • cargo test -- --nocapture - prints output from tests
  • cargo test 2>/dev/null - throws warning and errors away
  • cargo expand - view generated code from macros
  • Also, stacktraces from inside anything async are a pain!