Parametrized Testing
Sometimes, you want to run the same test logic with different inputs. Instead of duplicating code into separate test functions, you can use parameterized tests.
Parameterized tests allow you to define multiple test cases for a single function by attaching the
#[test_case]
attribute.
Each test case provides its own set of arguments, and snforge
will automatically generate separate test instances for them.
Basic Example
To turn a regular test into a parameterized one, add the #[test_case(...)]
attribute above it.
You can provide any valid Cairo expressions as arguments.
Below is a simple example which checks addition of two numbers.
fn sum(x: felt252, y: felt252) -> felt252 {
return x + y;
}
#[test]
#[test_case(1, 2, 3)]
#[test_case(3, 4, 7)]
fn test_sum(x: felt252, y: felt252, expected: felt252) {
assert_eq!(sum(x, y), expected);
}
Now run:
$ snforge test
Output:
Collected 2 test(s) from parametrized_testing_basic package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] parametrized_testing_basic_integrationtest::example::test_sum_1_2_3 ([..])
[PASS] parametrized_testing_basic_integrationtest::example::test_sum_3_4_7 ([..])
Tests: 2 passed, 0 failed, 0 ignored, [..] filtered out
Naming Test Cases
Each parameterized test gets its own generated name. There are two ways to control it:
-
Unnamed test case - the name is generated based on the function name and the arguments provided.
#[test_case(1, 2, 3)] fn test_sum(x: felt252, y: felt252, expected: felt252) { assert_eq!(sum(x, y), expected); }
This will generate a test named
test_sum_1_2_3
. -
Named test case - you can provide a custom name for the test case using the
name
parameter.#[test_case(name: "one_plus_two", 1, 2, 3)] fn test_sum(x: felt252, y: felt252, expected: felt252) { assert_eq!(sum(x, y), expected); }
This will generate a test named
test_sum_one_plus_two
.
📝 Note For unnamed test cases, it's possible that two different input values of the same type can generate the same test case name. In such cases we emit a diagnostic error. To resolve it, simply provide an explicit
name
for the case.
Advanced Example
Now let's look at an addvanced example which uses structs as parameters.
#[derive(Copy, Drop)]
struct User {
pub name: felt252,
pub age: u8,
}
#[generate_trait]
impl UserImpl of UserTrait {
fn is_adult(self: @User) -> bool {
return *self.age >= 18_u8;
}
}
#[test]
#[test_case(User { name: 'Alice', age: 20 }, true)]
#[test_case(User { name: 'Bob', age: 14 }, false)]
#[test_case(User { name: 'Josh', age: 18 }, true)]
fn test_is_adult(user: User, expected: bool) {
assert_eq!(user.is_adult(), expected);
}
Now run:
$ snforge test
Output:
Collected 3 test(s) from parametrized_testing_advanced package
Running 3 test(s) from tests/
[PASS] parametrized_testing_advanced_integrationtest::example::test_is_adult_user_name_alice_age_20_true ([..])
[PASS] parametrized_testing_advanced_integrationtest::example::test_is_adult_user_name_josh_age_18_true ([..])
[PASS] parametrized_testing_advanced_integrationtest::example::test_is_adult_user_name_bob_age_14_false ([..])
Running 0 test(s) from src/
Tests: 3 passed, 0 failed, 0 ignored, [..] filtered out
Combining With Fuzzer Attribute
#[test_case]
can be freely combined with the #[fuzzer]
attribute.
Below is an example in which we will fuzz the test but also run the specific defined cases.
fn sum(x: felt252, y: felt252) -> felt252 {
return x + y;
}
#[test]
#[test_case(1, 2)]
#[test_case(3, 4)]
#[fuzzer(runs: 10)]
fn test_sum(x: felt252, y: felt252) {
assert_eq!(sum(x, y), x + y);
}
Now run:
$ snforge test
Output:
Collected 3 test(s) from parametrized_testing_fuzzer package
Running 3 test(s) from tests/
[PASS] parametrized_testing_fuzzer_integrationtest::example::test_sum_1_2 ([..])
[PASS] parametrized_testing_fuzzer_integrationtest::example::test_sum_3_4 ([..])
[PASS] parametrized_testing_fuzzer_integrationtest::example::test_sum ([..])
Running 0 test(s) from src/
Tests: 3 passed, 0 failed, 0 ignored, 0 filtered out
Fuzzer seed: [..]