subtest
Just continue writing tests.
Ever got to a point where you felt extending a test would be much easier than writing a new one? But the thing you want to test is actually a different feature, and would warrant its own test? You could copy-paste the existing test's setup code, or you could refactor it to a shared test fixture, but you could also...
... use #[subtest]: a Rust macro to easily use test setup code from one test function in another one.
Example
Let's say you have a TODO list app.
use subtest;
Installation
or, in your Cargo.toml:
[]
= "1.0.0"
subtest requires Rust 1.85 or newer.
How it works
- Statements preceding a nested
#[subtest]function are copied into the nested function's body - This means you can freely use and mutate any local variables from the parent function in the nested function...
- ... without affecting the parent function or sibling test functions
- Statements following a nested
#[subtest]function are not copied - they only run in the parent function - The parent function stays a test of its own, and every subtest becomes a new test
The above example gets expanded to:
and therefore runs as three tests:
running 3 tests
test add_creates_pending_task ... ok
test add_creates_pending_task_subtests::complete_marks_task_completed ... ok
test add_creates_pending_task_subtests::cancel_marks_task_cancelled ... ok
Since subtests live in a <parent function>_subtests module, cargo test add_creates_pending_task runs the parent test together with all of its subtests, while cargo test add_creates_pending_task_subtests::complete_marks_task_completed runs just that single subtest.
What you can do
Arbitrarily nest test functions
You can nest #[subtest] functions arbitrarily deeply:
Each #[subtest] inherits the code of all parent functions, in order, as its setup code.
Use async tests
Example:
async
Make sure to mark nested #[subtest] functions async as well. You cannot downgrade from async back to sync.
You can, however, upgrade from sync to async!
Use rstest (or other testing frameworks)
Example:
The #[case]s you define in the top-level test function are applied to nested #[subtest]s as well.
Make sure to specify the #[subtest] attribute first, before #[rstest].
The same way you can use rstest, you can use any other testing framework as well.
Omit, add or override attributes, parameters, return types
Nested #[subtest]s' function attributes, parameters and return type are inherited from the parent test function by default.
The following:
async
is semantically equivalent to:
async
You may also add attributes, for example #[ignore] or #[should_panic]:
You may also override attributes, for example #[test] with #[tokio::test]:
Note: If you set inherit_attributes = false, you also have to repeat attributes like #[allow(clippy::<lint>)] on the nested subtest, if they were set on the parent.
Set them on the enclosing module instead to apply them unconditionally.
Things to be aware of
Attribute inheritance exceptions
Attribute inheritance is transitive: every attribute of a test function is passed on to its subtests, to their subtests, and so on.
There are three exceptions:
#[ignore]and#[should_panic]are not passed down- Doc comments are not passed down
- An
#[expect(<lint>)]is passed down as an#[allow(<lint>)], so that it is not reported as unfulfilled in a subtest which does not happen to trigger the lint
So marking a test #[ignore] does not ignore its subtests:
running 2 tests
test parent ... ignored
test parent_subtests::child ... ok
To ignore the subtests as well, mark each of them #[ignore] too.
For any other attribute, to apply it to the parent only, disable attribute inheritance on the nested subtest with #[subtest(inherit_attributes = false)] and re-specify the attributes you do want - including the test attribute.
Note that this only turns off attribute inheritance - setup code is still inherited as usual!
Do not omit test attribute altogether
Even when using #[subtest], you still have to specify an "actual" test attribute - typically #[test], or alternatively #[tokio::test] or #[rstest] (or whatever testing framework you intend to use) - at least for the top-level test function:
// <-- Do not omit this!
Just follow these rules:
- If it is a top-level function with a
#[subtest]attribute: Specify a#[test]attribute as well! - If it is a nested function with a
#[subtest]attribute (and optionally more attributes like#[should_panic]): You can omit the#[test]attribute, it is inherited (see Omit, add or override attributes, parameters, return types) - If it is a nested function with a
#[subtest(inherit_attributes = false)]attribute: Specify a#[test]attribute as well! - Always put other attributes after
#[subtest]
In case you do ever forget the #[test] attribute, you will get a compiler error:
error: function is missing a test attribute, such as #[test], #[tokio::test] or #[rstest]
add one below #[subtest] - attributes written above it are not visible to this macro
if this function is meant to be a nested subtest, add #[subtest] to the enclosing test function instead
--> tests/ui/fail/missing_test_attr.rs:4:4
|
4 | fn parent() {
| ^^^^^^
subtest determines test attributes by checking whether they end with test.
If your test attribute of choice does not, you can opt out of the check via:
// your weirdly-named test attribute goes here!
Setup code runs once per test, in parallel
Every subtest inherits the setup code preceding it. This means that the setup code runs once for the parent test, and once again for every subtest. Rust's test harness runs tests in parallel by default.
This breaks setup code which uses a shared resource like a fixed TCP port or a file at a fixed path. Example:
Both tests bind the same port, if they run at the same time one fails:
running 2 tests
test server_starts ... ok
test server_starts_subtests::server_accepts_a_connection ... FAILED
failures:
---- server_starts_subtests::server_accepts_a_connection stdout ----
called `Result::unwrap()` on an `Err` value: Os { code: 98, kind: AddrInUse, message: "Address already in use" }
The solution is to make the setup code safe to run concurrently with itself:
-
prefer resources which are unique per test: port
0(letting the OS pick a free port) instead of a fixed port or a temporary directory instead of a fixed path. Example: -
or, if the resource cannot be made unique, serialize the affected tests with a crate such as
serial_test:// <-- inherited by the subtest, so the two never run at the same time -
or, as a last resort, run the whole test binary single-threaded via
cargo test -- --test-threads=1
Ambiguous macro import
If you happen to use assert2's assert macro, or any other macro that has a name similar to a macro from the stdlib's auto-imported prelude, you will get a conflict compiler error when using the macro in a nested #[subtest].
Example: The following code:
use assert;
use subtest;
will lead to the following compiler error:
error[E0659]: `assert` is ambiguous
--> tests/ui/fail/readme_ambiguous_assert_import_example.rs:15:9
|
15 | assert!(value == "Hello!");
| ^^^^^^ ambiguous name
|
= note: ambiguous because of a conflict between a name from a glob import and an outer scope during import or macro resolution
note: `assert` could refer to the macro imported here
--> tests/ui/fail/readme_ambiguous_assert_import_example.rs:6:1
|
6 | #[subtest]
| ^^^^^^^^^^
= help: consider adding an explicit import of `assert` to disambiguate
= help: or use `self::assert` to refer to this macro unambiguously
note: `assert` could also refer to the macro defined here
--> $RUST/std/src/prelude/mod.rs
|
| pub use super::v1::*;
| ^^^^^^^^^
= note: this error originates in the attribute macro `subtest` (in Nightly builds, run with -Z macro-backtrace for more info)
This error is caused by the use super::* glob import in submodules generated for nested #[subtest] functions:
use assert;
There are two possible solutions for this:
-
Either import the macro within the subtest itself (or in the parent, if the parent also uses the macro or you have many subtests using it):
-
Or qualify the invocation with
super:
Unused variables
If you define a variable which is only used in nested subtests, but not in the parent test function, you will get an "unused variables" warning. Example:
will lead to
warning: unused variable: `receiver`
--> tests/ui/fail/readme_unused_variables_example.rs:10:18
|
10 | let (sender, receiver) = std::sync::mpsc::channel();
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_receiver`
|
The solution is to
-
either avoid declaring unused variables in the parent test function, and move them directly into the subtest that needs them (preferred)
-
or, if this is not possible (like in the example shown above), explicitly drop them at the end of the parent test function's scope:
Changelog
Refer to CHANGELOG.md
Maintenance
If you are a contributor, refer to MAINTENANCE.md for maintenance notes regarding this repository
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.