Defining tests

Enabling the test-r harness

Writing tests with test-r is very similar to writing tests with the built-in test framework, but there are a few differences.

Disabling the built-in test harness

First, for every build target where test-r is going to be used, the built-in test harness must be disabled.

This is done by putting harness = false in build target's section in Cargo.toml:

[lib]
harness = false

[[bin]]
harness = false

[[test]]
name = "integ-test-1"
harness = false

[[test]]
name = "integ-test-2"
harness = false

# ...

Mixing test-r and the built-in test harness

It is recommended to turn off running tests completely in the rest of the targets. For example if the crate produces both a library and an executable, and all the tests are in the library part, then put test = false in the [[bin]] section:

[[bin]]
test = false

[lib]
harness = false

Without this, cargo test will run all the test harnesses including the one where the built-in harness is not disabled ([[bin]] in this case), which may fail on some unsupported command line arguments that the test-r harness accepts.

If the intention is to use both test-r and the built-in test harness in the same crate, that's possible, but be careful with the command line arguments passed to cargo test as some of them may be only supported by the unstable version of the built-in test framework.

Enabling the test-r harness

For every target where the built-in harness was disabled (with harness = false), we need to install test-r's test runner instead. In other words, if the compilation is in test mode, we have to define a main function that runs the test-r test runner.

This can be done by adding the following macro invocation at the root of the given build target:

#![allow(unused)]
fn main() {
#[cfg(test)]
test_r::enable!();
}
  • For [lib] targets, this should be in src/lib.rs (or whatever crate root is specified)
  • For [[bin]] targets, this should be in the src/main.rs, src/bin/*.rs files or the one explicitly set in the crate manifest, for each binary
  • For [[test]] targets, this should be in the tests/*.rs files for each test

Writing tests

Writing tests is done exactly the same way as with the built-in test framework, but with using test-r's #[test] attribute instead of the built-in one. We recommend importing the test attribute with use test_r::test; so the actual test definitions look identical to the built-in ones, but it is not mandatory.

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use test_r::test;

    #[test]
    fn test_lib_function() {
        assert_eq!(lib_function(), 11);
    }
}
}

Within the test function itself any assertion macros from the standard library or any of the third-party assertion crates can be used. (All panics are caught and reported as test failures.)

Writing async tests

The same #[test] attribute can be used for async tests as well. The test runner will automatically detect if the test function is async and run it accordingly.

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use test_r::test;

    #[test]
    async fn test_async_function() {
        assert_eq!(async_lib_function().await, 11);
    }
}
}

Support for async tests requires the tokio feature, which is enabled by default.

There is a difference in how test-r runs async tests compared to how #[tokio::test] does. While tokio's test attribute spawns a new current-thread (by default) Tokio runtime for each test, test-r uses a single multi-threaded runtime to run all the tests. This is intentional, to allow shared dependencies that in some way depend on the runtime itself.

Tests returning Result

Tests in test-r can have a Result<_, _> return type. This makes it easier to chain multiple functions within the test that can return with an Err, no need to unwrap each. A test that returns a Result::Err will be marked as failed just like as if it had panicked.

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use test_r::test;

    #[test]
    fn test_lib_function() -> Result<(), Box<dyn std::error::Error>> {
        let result = lib_function()?;
        assert_eq!(result, 11);
        Ok(())
    }
}
}

Ignoring tests

The standard #[ignore] attribute can be used to mark a test as ignored.

#![allow(unused)]
fn main() {
#[test]
#[ignore]
fn ignored_test() {
    assert!(false);
}
}

Ignored tests can be run with the --include-ignored or --ignored flags, as explained in the running tests page.

Testing for panics

The #[should_panic] attribute can be used to mark a test as expected to panic. The test will pass if it panics, and fail if it doesn't.

#![allow(unused)]
fn main() {
#[test]
#[should_panic]
fn panicking_test() {
    panic!("This test is expected to panic");
}
}

Optionally the expected argument can be used to only accept panics containing a specific message:

#![allow(unused)]
fn main() {
#[test]
#[should_panic(expected = "expected to panic")]
fn panicking_test() {
    panic!("This test is expected to panic");
}
}