rstest-tests
Configures and runs rstest - Rust parametrized + fixture-based testing crate; `#[rstest]` attribute + `#[case(...)]` for parametrize; `#[fixture]` for reusable test setup; matrix tests via multiple `#[case]` × N (cartesian product); async test support via `#[async_std::test]` / `#[tokio::test]` + `#[rstest]`; `#[future]` for async fixtures. Use when working with Rust and needing parametrize/fixture patterns beyond stdlib `#[test]`.
Install with skills.sh (any agent)
npx skills add testland/qa --skill rstest-testsrstest-tests
Overview
Per github.com/la10736/rstest (opens in new window):
rstest is a Rust crate for parametrize + fixture testing - features that Rust's stdlib #[test] lacks. Pairs with cargo-test (rstest tests are still discovered by cargo test).
When to use
Step 1 - Install
Cargo.toml:
[dev-dependencies]
rstest = "0.21"Step 2 - Parametrize with #[case]
use rstest::rstest;
#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, 1, 0)]
#[case(100, 200, 300)]
fn add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}Each #[case] runs as a separate test - failures don't stop subsequent cases. Test names become add_cases::case_1, add_cases::case_2, etc.
Step 3 - Named cases
#[rstest]
#[case::positive(1, 2, 3)]
#[case::zero(0, 0, 0)]
#[case::negative(-1, 1, 0)]
fn add_named(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}Test names: add_named::positive, add_named::zero, etc. - much better for failure logs.
Step 4 - Fixtures
use rstest::{fixture, rstest};
#[fixture]
fn db() -> Database {
Database::new_test_instance()
}
#[fixture]
fn user(db: Database) -> User {
db.create_user("alice")
}
#[rstest]
fn test_user_id(user: User) {
assert_eq!(user.id, 1);
}
#[rstest]
fn test_user_email(user: User, db: Database) {
assert_eq!(db.find_user(user.id).email, "alice@example.com");
}Fixtures are functions with #[fixture]; tests with #[rstest] auto-receive them via parameter name matching. Fixture chaining works (a fixture can request other fixtures).
Step 5 - Matrix tests (cartesian product)
#[rstest]
fn test_matrix(
#[values("alice", "bob", "charlie")] name: &str,
#[values(0, 18, 65)] age: u32,
) {
let user = User::new(name, age);
assert!(user.is_valid());
}Runs 3 × 3 = 9 tests with all combinations. Equivalent to nested #[case] for cartesian product.
Step 6 - Combined #[case] + #[values]
#[rstest]
#[case::happy("alice")]
#[case::edge("a")]
fn test_with_values(
#[case] base_name: &str,
#[values(0, 1, 100)] count: u32,
) {
// happy × 3 values = 3 tests
// edge × 3 values = 3 tests
}Step 7 - Async tests
use rstest::rstest;
#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[tokio::test]
async fn async_add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
let result = add_async(a, b).await;
assert_eq!(result, expected);
}For async fixtures, use #[future]:
use rstest::*;
#[fixture]
async fn db_async() -> Database {
Database::connect_async().await.unwrap()
}
#[rstest]
#[tokio::test]
async fn test_async(#[future] db_async: Database) {
let db = db_async.await;
assert!(db.is_connected());
}Step 8 - Indirect fixtures (parameterize fixture instances)
#[fixture]
fn user(#[default("alice")] name: &str) -> User {
User::new(name)
}
#[rstest]
#[case::alice("alice")]
#[case::bob("bob")]
fn test_user(#[case] expected_name: &str, #[with(expected_name)] user: User) {
assert_eq!(user.name, expected_name);
}#[with(...)] injects custom args into the fixture for that test.
Step 9 - CI integration
Same as plain cargo test:
- run: cargo test --all-targetsrstest tests are discovered + run by cargo test natively - no separate runner.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Use rstest for single-case tests | Adds dependency for no benefit | Plain #[test] for single cases |
Skip #[case::name] | Test names become case_1, case_2; debug-hostile | Always name cases (Step 3) |
| Heavy matrix (5 dims × 5 values = 3125 tests) | Combinatorial explosion | Strategic cases vs full matrix |
| Mix sync + async tests in same parametrize | Confusing | Separate #[rstest] blocks per sync/async |
Limitations
References
Related skills
cargo-test
Configures and runs Rust's built-in `cargo test` - `#[test]` + `#[should_panic]` + `Result<(), E>` returns; integration tests in `tests/`; doc-tests embedded in `///` comments; `--lib` / `--bins` / `--all-targets` / `--workspace` selection; `cargo bench` (nightly) + Criterion (stable); `cargo test -- --test-threads=1` for serial; `cargo test -- --nocapture` to see println output. Use for any Rust project - testing is built into Cargo, no install needed.
ginkgo-tests
Configures and runs Ginkgo - Go BDD test framework with `Describe` / `Context` / `It` nesting; `BeforeEach` / `AfterEach` / `JustBeforeEach` / `JustAfterEach` lifecycle; Gomega matchers DSL (`Expect(actual).To(Equal(expected))`); parallel execution via `-p`; focus (`F` prefix) + skip (`P` prefix); `DescribeTable` + `Entry` for parametrized tests; `ginkgo` CLI tool. Use when working with Go on a BDD-style test suite (Kubernetes-ecosystem standard).
go-rust-mocking
Isolates dependencies in Go and Rust unit tests using test-double generation - Go: `go.uber.org/mock` (gomock + mockgen codegen) and `github.com/stretchr/testify/mock` (hand-written stubs); Rust: `mockall` crate with `#[automock]` for traits and `mock!` macro for structs and external traits. Use when a unit test reaches a database, HTTP client, file system, or any interface boundary that must be replaced with a controlled fake to keep tests fast, deterministic, and isolated.
go-test
Configures and runs Go's stdlib `testing` package - `func TestXxx(t *testing.T)` convention; table-driven tests via slice + range; `t.Run` for subtests with hierarchical names; `t.Parallel()` for parallel execution; benchmarks (`func BenchmarkXxx(b *testing.B)`); examples (`func ExampleXxx()`); fuzzing (`func FuzzXxx(f *testing.F)`); coverage via `go test -cover`/`-coverprofile`; build tags for selective compilation. Use for any Go project - testing is a stdlib feature, no install needed.