# test-case: generating named parameterised Rust tests from an attribute

> test-case is a Rust procedural macro attribute that turns one test function into many parameterised test instances, with optional names so each case appears as its own line in the test output, and a test_matrix variant that takes the Cartesian product of argument values. It suits a Rust developer who is tired of hand writing a wrapper per case, and it follows the crate's own policy of tracking only the latest stable Rust.

**frondeus/test-case** — Rust procedural macro attribute for adding test cases easily

- Repository: https://github.com/frondeus/test-case
- Stars: 635 · Forks: 43
- Language: Rust
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/frondeus-test-case

## One function, many test instances, each with a name

The test-case crate provides a procedural macro attribute that generates parametrized test instances. That is the whole idea, and the interesting part is the second half of it: each generated instance can carry a name.

The attribute is applied to an ordinary test function, and each attribute instance supplies the arguments plus an optional description after a semicolon.

```rust
#[cfg(test)]
mod tests {
    use test_case::test_case;

    #[test_case(-2, -4 ; "when both operands are negative")]
    #[test_case(2,  4  ; "when both operands are positive")]
    #[test_case(4,  2  ; "when operands are swapped")]
    fn multiplication_tests(x: i8, y: i8) {
        let actual = (x * y).abs();

        assert_eq!(8, actual)
    }
}
```

The output of cargo test shows what that buys, and it is not a cosmetic gain.

```sh
running 3 tests
test tests::multiplication_tests::test_case_1_when_both_operands_are_negative ... ok
test tests::multiplication_tests::test_case_2_when_both_operands_are_positive ... ok
test tests::multiplication_tests::test_case_3_when_operands_are_swapped ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```

Without a name, three cases of the same function would be reported as test_case_1, test_case_2 and test_case_3, and a failure would tell you a case number. With the name, the report tells you that the failure was when operands are swapped. In a suite with fifty cases that is the difference between a report you can read and one you have to decode.

The crate is MIT licensed, the primary language is Rust, and the last push was on 2026-09-23 with the v3.4.0 release the same day, after v3.3.1 and v3.3.0 in November 2023.

## test_matrix, and the Cartesian product of argument values

The second macro in the crate is test_matrix, and it exists for the case where the interesting space is the product rather than the list.

The README states the rule plainly: the number of arguments to the test_matrix macro must be the same as the number of arguments to the test function. Each macro argument can be one of three things. A list in array or tuple syntax, whose values can be any valid expression. A closed numeric range expression such as 0..100 or 1..=99, which generates argument values for all integers in the range. Or a single expression, which can be used to keep one argument constant while varying the other test function arguments using a list or a range.

```rust
#[cfg(test)]
mod tests {
    use test_case::test_matrix;

    #[test_matrix(
        [-2, 2],
        [-4, 4]
    )]
    fn multiplication_tests(x: i8, y: i8) {
        let actual = (x * y).abs();

        assert_eq!(8, actual)
    }
}
```

Two values for x and two for y produce four cases, which is the point. Writing those four cases by hand is the tedium this macro removes.

The single expression form is the part that makes the macro more useful than a simple list expansion. A test function with three arguments where only two vary can be written as a matrix over the two varying ones with the third held constant, rather than duplicating the constant into every list.

The cost of the matrix is combinatorial and it is worth being deliberate about it. Two lists of ten is a hundred cases, and a range of 0..100 against a list of ten is a thousand. Nothing in the macro limits that, so the number of arguments and the size of the ranges are the two things to check before adding a matrix to a suite that runs on every commit.

## Adding it to a project, and the import that is not optional

Getting set up takes two steps, and the second one catches people out.

The crate has to be added as a dependency to Cargo.toml, and the documented command is a development dependency.

```sh
cargo add --dev test_case
```

Then the macro has to be imported into the scope of the block where it is called, and the README explains why this is not automatic. The attribute name collides with rust's built-in custom_test_frameworks, so the import is what resolves the ambiguity.

```rust
use test_case::test_case;
```

That collision is the reason the import exists at all, and it is worth understanding rather than just copying. Rust has a built-in attribute called test that marks a function as a test, and a third party crate wanting to expose an attribute of the same name has to be brought into scope deliberately. A project with its own module named test_case would hit the same problem.

The import goes inside the test module, as the examples show, which means each test module that uses the attribute needs it. That is a small amount of repetition, and it is the price of the name being free for the macro author.

The Cargo.toml in the repository gives the rest of the picture. The package is named test-case with the version 3.4.0, the edition is 2024, the rust-version field asks for 1.85, and the workspace members are crates/*, so the macro implementation lives in a separate crate, test-case-macros, which the main crate depends on with default-features false. The published documentation is on docs.rs, and the crate is on crates.io with the usual version and download badges.

## The MSRV policy, and why it decides whether you can use this

The MSRV Policy section is short and it is the most consequential thing in the README for a team making an adoption decision.

Starting with version 3.0 and up, test-case introduces policy of only supporting latest stable Rust. These changes may happen overnight, so if your stack is lagging behind current stable release, it may be best to consider locking test-case version with = in your Cargo.toml.

That is an unusually explicit statement of a policy that is often left implicit. The reason it exists is structural rather than ideological. A procedural macro crate depends on the compiler's internal APIs, and those change between releases, sometimes in ways that break a macro without a major version bump being felt by users. The crate's own version number is 3.4.0, and a patch release of the macro crate can therefore break a build that was working yesterday.

The guidance in the README is practical. If your project tracks stable Rust closely, you take the latest version and get the fixes. If your toolchain is pinned, for example by a platform target, an embedded project or a team that upgrades in batches, you pin the exact version with the = operator in Cargo.toml, and you accept that you are pinning a version that will never get the next Rust release's fixes.

The Cargo.toml also carries a maintenance badge with the status actively-developed, and the repository has a CHANGELOG.md, a README.tpl template, a devenv.nix and devenv.yaml, and a crates directory. The recommended tools for contributors are listed too, with cargo readme to regenerate the README from the template and the lib.rs comments, cargo insta to review test snapshots, and the usual cargo fmt, cargo clippy and cargo fix.

So the crate is a small, well maintained project with an explicit compatibility stance, and the stance is a real constraint for anyone on an older toolchain.

## Where the naming stops, and what a test-case library cannot do

The feature that distinguishes this crate from a hand written parameterisation helper is the optional name, and it is worth being clear about what that does and does not give you.

A name is a string in the test report. It is not a value. When the case fails, the report tells you when operands are swapped, and you then look at the function to see which inputs that was. A different approach, used by some test frameworks, attaches the actual parameter values to the test name so the report reads as the data itself, and lets you filter or select cases by value. This crate does not do that, and the difference matters when a suite has hundreds of generated cases, because then a failure line is a description you have to interpret rather than a row you can look up.

The second limitation is the dependency on the macro layer. The crate is a procedural macro, so it runs at compile time and generates real Rust test functions rather than doing anything at runtime. That is the right design for a test framework, and it has the usual cost: the macro cannot inspect the function body, so anything that depends on the arguments being a specific shape, for example a literal rather than a computed expression, has to be handled by the macro's own parsing rather than by inference from the signature.

The third is the version constraint already discussed, which in practice is the one that decides the answer for a given project.

For the alternative, the honest comparison is with writing the cases by hand, which is what a lot of small test suites do and is entirely reasonable up to a dozen cases. The macro pays for itself at the point where the table stops being short enough to read comfortably, and it loses to a plain function when there are two or three cases, where a hand written table is clearer than the macro syntax and does not pull a dependency into the build.

## Conclusion

test-case fits a Rust developer writing table driven tests who wants each case to carry a readable name in the test report and who is comfortable with a macro that rewrites the function into several. It does not fit a project pinned to an old Rust toolchain, because the MSRV policy from version 3.0 is to support only the latest stable Rust and the changes may happen overnight, and it does not fit a team that wants the test names to carry data rather than prose. Before adding it, read the wiki, since the most up to date documentation lives there, and check the roadmap issue linked from the README if you need a feature that the current attribute set does not cover.

## FAQ

### How do I add test-case to a Rust project?

Add it as a development dependency with cargo add --dev test_case, then import the macro inside the module that uses it with use test_case::test_case, because the attribute name collides with rust's built-in custom_test_frameworks.

### What does the test_matrix macro do in the test-case crate?

It generates test cases from the Cartesian product of one or more possible values for each test function argument, and the number of arguments to the macro must match the number of arguments to the test function. Each macro argument can be a list in array or tuple syntax, a closed numeric range expression, or a single expression used to hold one argument constant while the others vary.

### Why does the test-case crate have an MSRV policy?

Starting with version 3.0 the crate only supports the latest stable Rust, and the README says these changes may happen overnight, so it recommends locking the test-case version with = in Cargo.toml if your stack lags behind the current stable release.

### What Rust version does the test-case crate require?

The Cargo.toml declares rust-version 1.85 and edition 2024, with the package at version 3.4.0. The macro implementation lives in a separate workspace crate, test-case-macros, which the main crate depends on.

## Sources

- [frondeus/test-case on GitHub](https://github.com/frondeus/test-case)
- [Issues](https://github.com/frondeus/test-case/issues)
- [License: MIT](https://github.com/frondeus/test-case/blob/master/LICENSE)
- [README](https://github.com/frondeus/test-case/blob/master/README.md)
- [Releases](https://github.com/frondeus/test-case/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/frondeus-test-case
