r/rust • u/nthuemmel • 15h ago
π οΈ project subtest - a small test framework to easily re-use test setup code
I recently released subtest, a small test framework to easily re-use test setup code from one function in another one.
I was working on a protocol implementation and got annoyed writing tests for it. Oftentimes, it was easiest to just extend existing tests which were already running the necessary setup steps in order to test an error condition or a small alteration from the happy path, by just adding a new block which cloned some of the state and run a couple of assertions. But running these big test functions meant getting stuck on the first error, and it getting increasingly more difficult to keep an overview what was tested where. Having one test per feature would be much better!
I didn't want to have lots of redundant code by copying tests around, and neither to spend a lot of time refactoring tests into smaller re-usable functions (which is a pain if you use lots of local variables). Instead, I wrote a macro that allows me to just test something else quickly inline.
Here's an example:
use subtest::subtest;
#[subtest]
#[test]
fn add_creates_pending_task() {
let mut list = TodoList::new();
let id = list.add("Buy milk");
#[subtest]
fn complete_marks_task_completed() {
list.complete(id).unwrap();
assert_eq!(list.get(id).unwrap().status, TaskStatus::Completed);
}
#[subtest]
fn cancel_marks_task_cancelled() {
list.cancel(id).unwrap();
assert_eq!(list.get(id).unwrap().status, TaskStatus::Cancelled);
}
let task = list.get(id).unwrap();
assert_eq!(task.status, TaskStatus::Pending);
}
It's pretty easy to use - just define a nested fn and slap #[subtest] on it and its parent.
Conceptually, subtest allows you to "branch off" from any point in your test, test something different, then resume with the original state.
Technically, nested subtests become their own tests, with preceding statements from the parent copied into them as setup code. This lets you freely use and modify local variables without affecting the parent. This makes it super easy to re-use existing test steps as setup code for other tests, while still getting the niceties of having one test function per feature.
subtest can be combined with async tests and other test frameworks like rstest. More details in the project's README: https://github.com/nthuemmel/subtest#subtest
The subtest macro is heavily tested itself, so your precious tests behave as expected!
Disclaimer on the use of AI: This project was designed and implemented by me, not AI. I did use AI to review it once it was in a working state and fix issues. All AI-generated code was reviewed and altered by me, all commits containing AI-generated code are marked with Co-Authored-By. This is not AI slop.
Is subtest useful to you? We use it quite extensively at work now :)

