Expand description
§Kitest
Kitest is a toolkit to build your own test harness for cargo test.
It provides everything you expect from the built-in Rust test harness out of the box, while allowing you to replace or customize each part independently. You only touch the pieces you care about and leave the rest to Kitest.
With a bit of macro machinery, Kitest can act as a drop in replacement for the default test harness. At the same time, it enables testing setups that go beyond what the built-in harness supports.
Kitest is designed as a foundation. It is not meant to enforce a single testing style, but to let you build one that fits your needs. Other crates can be built on top of it to provide fully packaged test harnesses or reusable components.
§What it looks like
A regular test run using the default harness and formatter:
running 3 tests
test test_b ... ignored, we don't need this
test test_c ... ok
test test_a ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 filtered out; finished in 0.30s
Grouped tests with shared setup and teardown:
running 5 tests
group b, running 2 tests
test b ... ok
test e ... ok
group a, running 3 tests
test a ... ok
test c ... ok
test d ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 filtered out; across 2 groups, finished in 0.00s
§What Kitest enables
- A test harness comparable to the built-in one, ready to use by default
- Data driven testing, tests do not need to originate from Rust code
- Grouping tests and running them with shared preparation and teardown
- Preparing and tearing down the entire test suite, not just individual tests
- Replacing individual parts of the harness without rewriting everything
Compared to other solutions:
- Unlike
libtest-mimic, every part of the harness can be replaced independently - Unlike
rstest, preparation and teardown are not limited to individual tests
§When should I use Kitest?
Kitest is a good fit if the built-in Rust test harness starts getting in your way.
You might want to use Kitest if:
- You want to customize how tests are discovered, filtered, or executed
- You need test suite level setup and teardown
- You want to group tests and run them with shared state or resources
- Your tests are data driven or come from non-Rust sources
- You want full control over test output and formatting
Kitest is also useful if you are building tooling on top of Rust testing. It provides the building blocks needed to construct higher level test frameworks without having to reimplement a full harness from scratch.
You may not need Kitest if:
- The built-in test harness already does everything you need
- You only need per test setup and teardown, then
rstestshould be enough - Your test setup heavily diverges from the typical behavior of
cargo test
Kitest focuses on flexibility and composability. If you want a single opinionated testing style out of the box, a higher level framework built on top of Kitest may be a better fit.
§Build your own test harness
The main entry point is harness. It takes a list of tests and returns a TestHarness
configured with a set of default strategies. From there, we can swap out individual parts
by chaining setter calls on the harness.
§Tests and metadata
A test is represented by Test<Extra>.
It combines two things:
- The test function itself (how the test is executed)
- The test metadata stored in
TestMeta<Extra>
TestMeta<Extra> contains the things a harness typically needs to know about
a test, like its name, whether it is ignored (optionally with a reason), and whether it is
expected to panic.
It also carries an extra field of type Extra for user defined metadata.
Extra is not a boxed trait object and it is not erased at runtime.
It is compiled statically, which keeps things fast and keeps the types precise.
Nearly everything in Kitest is generic over Extra, so our strategies can depend on metadata
however we like, without needing up- or downcasting.
This means Kitest does not decide what “tags”, “flags”, or “categories” mean. You decide that, and your harness strategies can use it directly.
§The test lifetime 't
Most harness types are generic over a lifetime called 't.
This is the lifetime of the tests passed into harness.
It is a core part of the design.
't is threaded through the harness and its strategies so that everything is allowed to borrow
from the original test list.
As long as a strategy only needs to look at tests or metadata, it can borrow instead of copying.
This avoids a lot of unnecessary allocations and makes it cheap to build higher level logic
around the test list.
In other words, 't is “the lifetime of the tests”, and all harness components effectively live
inside that boundary.
The harness still expects the full list of tests up front. After that, the harness can:
- filter tests (remove them from the run)
- ignore tests (keep them in the list, but do not execute them)
But it cannot add new tests dynamically once the harness is created.
This does not mean tests must be hardcoded. The initial list can be created from anything: reading files, scanning directories, querying a server, generating cases, and so on.
§Harness strategies
A TestHarness is composed of multiple strategies.
Each strategy is responsible for one part of how tests are handled.
We can keep the defaults and replace only what we need.
The default harness is assembled roughly like this:
- Filter: decides which tests participate in the run at all (for example, by name)
- Ignore: decides whether a participating test is executed or reported as ignored
- Panic handler: executes the test and converts panics into a test status
- Runner: schedules tests, usually in parallel, and collects outcomes
- Formatter: prints progress and results in a
cargo teststyle format
If you want to group tests, you can explicitly opt into grouping by calling
with_grouper on a TestHarness.
This turns it into a GroupedTestHarness.
From that point on, grouping-specific strategies become available and can be configured independently from the non-grouped harness.
A grouped harness adds a few more strategy points:
- Grouper: assigns tests to groups and can attach per-group context
- Group runner: runs groups and can stop early depending on the group’s outcome
- Grouped formatter: prints group level events and outcomes
This explicit transition keeps the basic harness simple, while making grouping a deliberate choice. Once grouping is enabled, all test execution happens through groups, which makes it possible to share setup and teardown logic and control execution flow at the group level.
§Example
This example shows how to build a custom test harness for your use case.
First, Cargo needs to be told not to use the built-in test harness. For unit tests, this can be done like this:
[lib]
bench = false
harness = false
And for integration tests like this:
[[test]]
name = "tests"
path = "tests/main.rs"
harness = false
By setting harness to false, we tell Cargo to skip the built-in harness.
Instead, it expects a custom main function that runs the tests.
use std::process::Termination;
use kitest::prelude::*;
fn main() -> impl Termination {
// However you collect your tests. This can be static or data driven.
let tests = collect_tests();
// At this point, you could parse command line arguments and
// construct different harnesses depending on them.
// The harness also supports a list mode via `list`.
kitest::harness(&tests)
.with_filter(MyFilter::new())
.with_runner(MyRunner::new())
.run()
.report()
}§Argument parsing and configuration
Kitest does not provide argument parsing.
Crates like clap,
lexopt,
bpaf,
or argh
already handle command line parsing well and integrate naturally with a custom main function.
The intended workflow is:
- Parse command line arguments in
main. - Collect tests.
- Decide which harness configuration to build.
- Run that harness.
Depending on the parsed arguments, you might:
- Switch between different formatters
- Enable or disable grouping
- Run in list mode instead of executing tests
- Choose different filters or runners
Kitest is optimized for compiling a concrete harness configuration. Strategy types are part of the harness type, which keeps everything static and efficient. This also means strategy implementations are not swapped dynamically at runtime.
If different configurations are possible, define them explicitly and select one based on the parsed arguments:
let args = parse();
let tests = collect();
match args.mode {
Mode::Run => {
kitest::harness(&tests).run().report();
},
Mode::List => {
kitest::harness(&tests).list();
}
}Argument parsing lives outside of Kitest. Kitest focuses only on building and running test harnesses.
Modules§
- capture
- Output capture.
- filter
- Test filtering for kitest.
- formatter
- Formatting output for kitest.
- group
- Grouping support for kitest.
- ignore
- Test ignoring for kitest.
- outcome
- Test outcomes.
- panic
- Panic handling for kitest.
- prelude
- Prelude containing everything you need to build a
Test. - runner
- Test execution and scheduling for kitest.
- test
- Test definitions.
Macros§
Structs§
- Grouped
Test Harness - A test harness that executes tests in groups.
- Grouped
Test Report - The report produced by running a
GroupedTestHarness. - Test
Harness - A configurable test harness.
- Test
List Report - The report produced when listing tests.
- Test
Report - The report produced by running a
TestHarness. - Whatever
- Type erased user data with a fixed set of trait bounds.
Functions§
- harness
- Build a
TestHarnessfrom a list of tests.
Type Aliases§
- Grouped
Test Outcomes - Collected outcomes of a grouped test run.
- Test
Outcomes - Collected outcomes of a test run.