#testing #parallel-testing #run #attributes #async-test #macro #guard

test-with-tokio

attribute macro for tests using tokio with cases and async guards

7 releases

0.3.3 Jan 9, 2023
0.3.2 Jan 8, 2023
0.2.1 Dec 31, 2022
0.1.0 Dec 23, 2022

#898 in Asynchronous

MIT license

8KB

Workflow Status

An attribute macro for tests using tokio with cases and async guards.

This crate provides a single polite attribute macro #[test_with_tokio::please] which allows you to write tests that do some not-async code before running async code within tokio. This is similar to #[tokio::test] but with two features: async code can be run prior to the tokio runtime being started, and a single test can be written to generate multiple tests handling multiple cases of the same test. With a bit of work, this enables you to run most of your tests in parallel, but to have a few that cannot be run concurrently.

Examples

At the most basic level, this crate enables you to easily write tests that run non-async code that will be run prior to async code.

// The async in `async fn` below is optional and ignored.
#[test_with_tokio::please]
async fn test_me() {
    println!("This code will be run before the tokio runtime is started.");
    async_std::println!("This code will be run with a tokio runtime").await;
}

Holding a lock

The motivating reason for this crate is to enable use of a lock to run tests concurrently:

static DIRECTORY_LOCK: std::sync::RwLock<()> = std::sync::RwLock::new(());

#[test_with_tokio::please]
fn test_run_exclusively() {
    let _guard = DIRECTORY_LOCK.write().unwrap();
    async_std::println!("This code will be run with exclusive access to the directory.").await;
}

#[test_with_tokio::please] fn test_run_cooperatively() {
    let _guard = DIRECTORY_LOCK.read().unwrap();
    async_std::println!("This code will be run concurrently with other cooperative tests..").await;
}

You might wonder, why not take the lock within the async block, or perhaps simply within a function marked with #[tokio::test]? The answer lies in the lack of an async Drop. This means that a test may not be fully cleaned up until after the tokio runtime exits, which is after the body of your test function has exited and released the lock, meaning you may still have race conditions in your tests, with a lock taken concurrently.

Multiple cases

If you can write code that generates multiple related tests by assigning a variable to match CASE { ... } where each case matches a string literal that is a valid suffix for an identifier.

#[test_with_tokio::please]
fn test_contains() {
    let container = match CASE {
        "hello" => "hello world",
        "this_test" => vec!["this_test"],
    };
    assert!(container.contains(CASE));
}

This example will create two functions each marked #[test], one named test_contains_hello and the other test_contains_this_test. The body of the first function will look like:

#[test]
fn test_contains_hello() {
    const CASE: &str = "hello";
    let container = "hello world";
    assert!(container.contains(CASE));
}

Dependencies

~1.5MB
~37K SLoC