Skip to content

#[tanu::test] Attribute

The #[tanu::test] attribute is used to mark functions as test functions in the Tanu framework. When a function is annotated with this attribute, it will be executed as part of the test suite.

Usage

To use the #[tanu::test] attribute, simply add it above the function definition:

use tanu::check_eq;

#[tanu::test]
async fn my_test_function() -> eyre::Result<()> {
    let result = 2 + 2;
    check_eq!(result, 4);
    Ok(())
}

Notes * The #[tanu::test] attribute can only be applied to "async" functions. * Functions marked with #[tanu::test] should not take any arguments and should not return any values. * The tanu framework will automatically discover and run all functions marked with the #[tanu::test] attribute when the test suite is executed.

Parameterized Tests

#[tanu::test] attribute is inspired by test_case crate where you can easily parameterize test case by providing parameters in the attribute body as follows.

use tanu::check_eq;

#[tanu::test(10, 10, 20)]
#[tanu::test(20, 20, 40)]
async fn my_test_function(a: u32, b: u32, expected: u32) -> eyre::Result<()> {
    let result = a + b;
    check_eq!(result, expected);
    Ok(())
}

If you run a parameterized test, the test name will be automatically generated by concatenating the stringified parameters, resulting in my_test_function_10_10_20.

Sometimes parameters can't be strigified, in such case, tanu refuse to compile the code. In such cases, tanu refuses to compile the code. If that happens, you can specify your desired name rather than the auto-generated one. Note that a test name is delimited by ; from the parameters.

use tanu::check_eq;

#[tanu::test(10, 10, 20; "add_10_and_10_equal_20")]
#[tanu::test(20, 20, 40; "add_20_and_20_equal_40")]
async fn my_test_function(a: u32, b: u32, expected: u32) -> eyre::Result<()> {
    let result = a + b;
    check_eq!(result, expected);
    Ok(())
}

Serial Execution

By default, Tanu runs tests in parallel for better performance. However, some tests need to run sequentially, such as tests that:

  • Share mutable state (databases, files, environment variables)
  • Modify global resources
  • Have ordering dependencies

The serial attribute allows you to control test execution order.

Basic Serial Execution

Use serial to run tests sequentially with all other serial tests:

#[tanu::test(serial)]
async fn database_setup() -> eyre::Result<()> {
    // This test runs serially with all other serial tests
    Ok(())
}

#[tanu::test(serial)]
async fn database_cleanup() -> eyre::Result<()> {
    // Runs after database_setup completes
    Ok(())
}

Named Serial Groups

Create named groups to isolate serial execution. Tests in different groups can run in parallel:

#[tanu::test(serial = "database")]
async fn db_write() -> eyre::Result<()> {
    // Serializes only with other "database" group tests
    Ok(())
}

#[tanu::test(serial = "database")]
async fn db_read() -> eyre::Result<()> {
    // Runs after db_write within the database group
    Ok(())
}

#[tanu::test(serial = "cache")]
async fn cache_write() -> eyre::Result<()> {
    // Can run in parallel with database group
    Ok(())
}

Serial with Parameters

The serial attribute can be combined with parameterized tests. The serial keyword can appear anywhere in the attribute arguments:

// Serial before parameters
#[tanu::test(serial, 200)]
#[tanu::test(serial, 404)]
async fn test_status_codes(code: u16) -> eyre::Result<()> {
    Ok(())
}

// Serial after parameters
#[tanu::test(1, 2, serial)]
#[tanu::test(3, 4, serial)]
async fn test_addition(a: i32, b: i32) -> eyre::Result<()> {
    Ok(())
}

// Named groups with parameters
#[tanu::test(serial = "api", 200)]
#[tanu::test(serial = "api", 404)]
async fn test_api_codes(code: u16) -> eyre::Result<()> {
    Ok(())
}

Key Points

  • Project-scoped: Serial groups are scoped per project. Same group name in different projects won't interfere.
  • Concurrency control: Serial tests acquire their group mutex before the global concurrency semaphore, preventing resource blocking.
  • Minimal lock scope: The serial lock only covers test execution, not setup, teardown, or retry logic.
  • Performance: Non-serial tests and different serial groups run in parallel for optimal performance.