Crate test_with_tokio
source ·Expand description
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));
}