---
title: How to use test-case crate for parameterized tests
summary: Use the test-case crate to run a single test function with multiple argument sets via the #[test_case] macro.
lang: en
slug: how-to-use-test-case-crate-for-parameterized-tests
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-test-case-crate-for-parameterized-tests/
---

# How to use test-case crate for parameterized tests



## When test functions start looking identical

You wrote a function that validates user input. The first test passes. You add a second test for a different edge case. It passes. By the fifth test, your file looks like a wall of nearly identical functions. You change the input, you change the expected output, the assertion logic stays the same. Copy-pasting test functions creates technical debt faster than the code you're testing. You end up with `test_valid_email`, `test_invalid_email`, `test_empty_email`, and when the validation logic changes, you have to update six separate test functions and hope you didn't miss one.

## Separating data from logic

Parameterized tests solve this by decoupling the test data from the test logic. You write the function once. You provide a list of inputs and expected outputs. The test runner executes the function for every row in that list. Think of it like a spreadsheet where column A is the input, column B is the expected result, and the formula in column C checks if they match. The `test-case` crate brings this pattern to Rust using a macro that generates the boilerplate for you.

Separate the data from the logic, and your tests become a specification you can scan, not a maze you have to navigate.

## Minimal example

Add the crate to your `Cargo.toml` as a development dependency. Tests are tools for building the project, not part of the shipped binary.

```bash
cargo add test-case --dev
```

Import the macro and decorate your test function. The syntax inside the attribute mirrors the function signature. Inputs come first, followed by `=>` and the expected return value.

```rust
use test_case::test_case;

/// Adds two integers and returns the sum.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

// The macro generates a separate test function for each attribute.
// The syntax is `inputs => expected_output`.
// The string after the semicolon is the test name suffix.
#[test_case(1, 2 => 3; "basic addition")]
#[test_case(-5, 5 => 0; "cancellation")]
#[test_case(0, 0 => 0; "identity")]
fn test_add(a: i32, b: i32) -> i32 {
    // The function body runs for every case.
    // The macro asserts the return value matches the expected output.
    add(a, b)
}
```

Write the function once, list the cases, and let the macro handle the repetition.

## What happens at compile time

The `#[test_case]` macro runs during compilation. It scans the attributes above the function and emits new code. If you have three attributes, the macro generates three functions: `test_add_basic_addition`, `test_add_cancellation`, and `test_add_identity`. Each generated function calls your original `test_add` with the specific arguments and asserts the result matches the expected value.

When you run `cargo test`, the test runner discovers these generated functions and executes them individually. If one fails, the output shows the specific suffix, so you know exactly which case broke. The macro hides the repetition but exposes the failure. You get the signal of a single test function with the granularity of many.

## Realistic patterns with Result and structs

Real code rarely returns simple integers. Functions return `Result`, `Option`, or complex structs. The crate handles these types naturally because the macro relies on `PartialEq` for assertions. If your type implements `PartialEq`, you can use the `=>` syntax directly.

```rust
use test_case::test_case;

/// Parses a temperature string into Celsius.
fn parse_temp(input: &str) -> Result<f64, &'static str> {
    input
        .trim()
        .parse::<f64>()
        .map_err(|_| "Invalid number")
}

// Testing successful parses.
// Result implements PartialEq, so the macro can assert Ok vs Err.
#[test_case("20.5" => Ok(20.5); "decimal")]
#[test_case("-5" => Ok(-5.0); "negative")]
#[test_case("  30  " => Ok(30.0); "whitespace")]
fn test_parse_success(input: &str) -> Result<f64, &'static str> {
    parse_temp(input)
}

// Testing failures.
// The macro compares the Err variant and the error message.
#[test_case("abc" => Err("Invalid number"); "letters")]
#[test_case("") => Err("Invalid number"); "empty")]
fn test_parse_failure(input: &str) -> Result<f64, &'static str> {
    parse_temp(input)
}
```

You can also pass structs as inputs. This is useful when your function takes a configuration object or a domain model. Derive `PartialEq` if you need to assert the struct as output, or just pass it as input if the function returns a scalar.

```rust
use test_case::test_case;

/// Represents a user configuration.
#[derive(Debug, PartialEq)]
struct Config {
    timeout: u32,
    retries: u32,
}

/// Validates configuration constraints.
fn validate_config(c: &Config) -> bool {
    c.timeout > 0 && c.retries < 10
}

// Struct literals work directly in the attribute.
// The macro infers the type from the function signature.
#[test_case(Config { timeout: 1, retries: 5 } => true; "valid config")]
#[test_case(Config { timeout: 0, retries: 5 } => false; "zero timeout")]
#[test_case(Config { timeout: 10, retries: 10 } => false; "max retries")]
fn test_config(c: &Config) -> bool {
    validate_config(c)
}
```

Parameterized tests shine when the edge cases multiply. List them all, and you catch the regression before it ships.

## Pitfalls and compiler errors

The macro expands to code that calls your function. If the types in the attribute don't match the function signature, the compiler rejects the expansion. You'll see `E0308` (mismatched types) if you pass a `String` where the function expects `&str`, or if the expected output type differs from the return type. The macro also requires the function to return a value that implements `PartialEq` if you use the `=> expected` syntax. If your function returns `()`, you can only use the attribute to pass arguments, not assert a result. In that case, write the assertions manually inside the function body.

Convention aside: always add the suffix string after the semicolon. `#[test_case(1, 2 => 3; "one plus two")]`. Without the suffix, the test names become generic and hard to read in failure output. The suffix makes the test report actionable. Another convention: keep `test-case` in `dev-dependencies`. Running `cargo add test-case --dev` ensures the crate doesn't bloat your production binary.

Always name your cases. A failed test without a name is a bug hunt without a map.

## When to use test-case versus alternatives

Use `test-case` when you have a function with clear inputs and outputs and you want to verify multiple specific scenarios without duplicating the assertion logic. Use doc tests when you need the example to serve as documentation for the public API and want to ensure the docs stay accurate. Use `proptest` when you want to generate random inputs and check invariants rather than verifying fixed cases. Use `#[should_panic]` alone when you only have one case where a panic is expected and parameterization adds no value.

Pick the tool that matches the shape of your data. Specific cases get `test-case`. Random chaos gets `proptest`.

## Where to go next

- [How to use doc tests](/en/how-to-use-doc-tests/)
- [How to use proptest for property testing](/en/how-to-use-proptest-for-property-testing/)
- [How to Test for Expected Panics with #[should_panic]](/en/how-to-test-for-expected-panics-with-shouldpanic/)