| Crates.io | spoke |
| lib.rs | spoke |
| version | 0.0.3 |
| created_at | 2025-10-31 09:32:13.870907+00 |
| updated_at | 2025-12-09 14:20:08.740389+00 |
| description | [coming soon] Simplified unit testing for Rust. |
| homepage | |
| repository | https://github.com/dgkimpton/spoke/tree/main |
| max_upload_size | |
| id | 1909764 |
| size | 121,949 |
Human readable test case writing for Rust
[!WARNING]
This library is not yet production-ready.Feedback and suggestions welcomed
Spoke::test! is a proc-macro for the Rust programming language to reduce the time and effort involved in writing tests.
The macro transforms the simplified syntax into standard Rust #[test] functions but saves significant typing.
| Available now | Coming soon | |
|
These features are on the roadmap but not yet available:
|
for planned features see TODO
Add the crate as a dependency
at the command line
$ cargo add spoke |
or in cargo.toml add[dependencies]
spoke = { version = "0.0.3" }
|
Then in your Rust code (e.g. main.rs ) you can add test cases inside a call to spoke::test!, e.g.
spoke::test!{
$"result is true" true;
}
Spoke::test! resolves around the $ symbol and what follows it to define nested sequential tests and assertions.
Test names are introduced as strings using $"" which enables requirements capture without trying to introduce underscores between each word.
Nested tests concatenate the names to produce unique test names. Spoke::test! converts these human-readable strings into function names.
You can imagine nested tests creating a sort of tree structure, each leaf of the tree becomes a unique test function (leaves are normally assertions).
Each test name is followed by either a body {} or an assertion which is ended with a ;.
Within a body any code not being preceded by a $ is included in that test (and in any nested tests).
Nesting requirement bodies allows for creation of sequential tests - that is, multiple tests that used the same setup but validate different assertions.
spoke::test!{
$"The user" {
let mut user = User::new();
$"is initially not logged in" !user.is_logged_in();
$"can be logged in with a secret" {
user.login("secret_token");
$"and is then logged in" user.is_logged_in();
$"and then logging out" {
let result = user.logout();
$"is ok" result;
$"leaves the user logged out" !user.is_logged_in();
}
}
$"trying to log out before login" {
let result = user.logout();
$"fails" !result;
$"leaves the user still logged out" !user.is_logged_in();
}
}
}
// becomes
#[cfg(test)]
mod spoketest {
#[test]
fn The_user_is_initially_not_logged_in(){
let mut user = User::new();
assert!(!user.is_logged_in());
}
#[test]
fn The_user_can_be_logged_in_with_a_secret_and_is_then_logged_in(){
let mut user = User::new();
user.login("secret_token");
assert!(user.is_logged_in());
}
#[test]
fn The_user_can_be_logged_in_with_a_secret_and_then_logging_out_is_ok(){
let mut user = User::new();
user.login("secret_token");
let result = user.logout();
assert!(result);
}
#[test]
fn The_user_can_be_logged_in_with_a_secret_and_then_logging_out_leaves_the_user_logged_out(){
let mut user = User::new();
user.login("secret_token");
let result = user.logout();
assert!(!user.is_logged_in());
}
#[test]
fn The_user_trying_to_log_out_before_login_fails(){
let mut user = User::new();
let result = user.logout();
assert!(!result);
}
#[test]
fn The_user_trying_to_log_out_before_login_leaves_the_user_still_logged_out(){
let mut user = User::new();
let result = user.logout();
assert!(!user.is_logged_in());
}
}
Sometimes it is necessary to introduce use statements to pull in other crates, this can be done inside the spoke::test! call and is generated as an internal preamble at the start of the test module
spoke::test!{
use std::f64::consts::*;
$"the standard constants module" {
$"contains a definition of Pi" PI $eq 3.14159265358979323846264338327950288_f64;
$"contains a definition of Tau" TAU $eq 6.28318530717958647692528676655900577_f64;
}
}
// becomes
#[cfg(test)]
mod spoketest {
use std::f64::consts::*;
#[test]
fn the_standard_constants_module_contains_a_definition_of_pi(){
assert_eq!(PI,3.14159265358979323846264338327950288_f64);
}
#[test]
fn the_standard_constants_module_contains_a_definition_of_tau(){
assert_eq!(TAU,6.28318530717958647692528676655900577_f64);
}
}
The simplest assert is written as a named requirement followed by a boolean expression.
$"requirement" <expression>;
which maps to a standard Rust assertion like
assert!(<expression>);
and the requirement is folded into the test name.
Simple assertion example
$"value should be square" is_square(4);
// becomes
#[test]
fn value_should_be_square() {
assert!(is_square(4));
}
Rusts equality assertions are also supported using an infix notation $eq and $ne.
$"requirement" <expression_1> $eq <expression_2> ;
which maps to a standard Rust assertion like
assert_eq!( <expression_1> , <expression_2> );
and the requirement is folded into the test name.
Equality assertion example
$"multiplication" {
let a = 2;
$"2 times 2 = 4" a*a $eq 4;
}
// becomes
#[test]
fn multiplication_2_times_2_equals_4() {
let a = 2;
assert_eq!(a*a,4);
}
So you tried spoke::test! and decided you don't like it? No problem.
Open your Rust file in vscode, navigate to the call to spoke::test! and then right-click, "refactor", "inline macro". The call to spoke::test! will be replaced with the generated tests and you can then remove spoke from your cargo.toml and carry on as if you had never used it.
Definitely not. spoke::test! is helpful in some scenarios but you should pick the testing methodology that best captures the requirements. Feel free to mix and match some tests with spoke::test! and some the standard way.
There are currently no optional features.
Due to limitations of the proc-macro (and proc-macro2) libraries on stable some of the compile errors are highlighted against a single token when they realistically apply to multiple tokens. Improvements can be made here when the proc_macro_span feature stabilises.
If any test in a spoke requires the variable to be mutable it must be mutable for all. This can sometimes cause issues. The workaround is to split up the branches which can mean undesired duplication. Until such time as we get proper reflection I don't have a perfect solution for this (ideally the test would just drop unused mutability based on compiler feedback).
Currently the following features of standard Rust tests are planned but as yet unavailable.
If you have thoughts, suggestions, or concerns please feel free to create a Discussion or directly raise an Issue.
This project uses the MIT license and that license shall automatically apply to any contributions made.