🎓 A1Learner
🔍
0 of 149 lessons completed

Rust Property Tests

A property test is a test that states a rule and lets the machine pick the cases. The machine tries hundreds of cases against the rule.

One test, one case

A normal test checks the cases you thought of. This one checks the score 50:

#[test]
fn clamp_keeps_50() {
    assert_eq!(clamp_score(50), 50);
}

State the rule instead

proptest! turns a rule into a test. The strategy any::<i32>() feeds in 256 numbers, one at a time:

proptest! {
    #[test]
    fn clamp_stays_in_range(n in any::<i32>()) {
        let r = clamp_score(n);
        prop_assert!(r >= 0 && r <= 100);
    }
}

Strategies build bigger things

A strategy can build whole vectors. This rule checks the output of a sort:

proptest! {
    #[test]
    fn sorted_output_is_sorted(values in prop::collection::vec(any::<i32>(), 0..50)) {
        let sorted = sort_copy(values);
        prop_assert!(sorted.windows(2).all(|pair| pair[0] <= pair[1]));
    }
}

A failure shrinks

This rule sounds safe. One more is always bigger. Aim the strategy near the top edge and the run fails. The report ends with the smallest failing case. Adding 1 to the biggest i32 overflows, so the rule breaks at exactly one value:

proptest! {
    #[test]
    fn one_more_is_bigger(x in (i32::MAX - 100)..=i32::MAX) {
        prop_assert!(x + 1 > x);
    }
}

Keep the interesting cases

Turn the edges into plain tests. They run in a blink. They guard the exact spots the machine found:

#[test]
fn clamp_handles_both_edges() {
    assert_eq!(clamp_score(i32::MAX), 100);
    assert_eq!(clamp_score(i32::MIN), 0);
}

Example

fn clamp_score(n: i32) -> i32 {
    if n < 0 {
        0
    } else if n > 100 {
        100
    } else {
        n
    }
}

fn next_number(state: &mut u64) -> i32 {
    // A tiny fixed generator. The same steps run every time, so the checks never change.
    *state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
    ((*state >> 33) % 2001) as i32 - 1000
}

fn main() {
    let mut state = 42u64;
    let mut checked = 0;
    let mut all_in_range = true;
    for _ in 0..500 {
        let n = next_number(&mut state);
        let r = clamp_score(n);
        if !(0..=100).contains(&r) {
            all_in_range = false;
        }
        checked += 1;
    }
    println!("cases checked = {checked}");
    println!("every result in 0..=100 = {all_in_range}");
}

Key points

  • Property: a rule that must hold for every input.
  • It is not a list of examples.
  • Strategy: the recipe that builds the inputs.
  • any::<i32>() builds any i32.
  • A range builds only inside itself.
  • Use prop_assert! and prop_assert_eq! inside a property test.
  • They work like assert! and assert_eq!.
  • Shrinking: after a failure, proptest tries smaller and smaller inputs.
  • It reports the smallest case that still fails.
  • proptest is a dev-dependency.
  • Add it under [dev-dependencies].
  • Shipped code never includes it.
  • Aim a strategy at the edge when you know where it is.
  • A property test checks a rule against machine-picked cases instead of hand-picked examples.

In this example, a tiny generator makes the numbers. It follows fixed steps, so every run checks the same 500 cases. Each case goes through clamp_score. The programme only remembers one thing. Did every result stay inside 0 to 100? The answer prints as true. Real proptest does this job far better. It picks the cases at random. It also shrinks a failure down to the smallest case. This hand-made version shows the shape of the idea with no crates at all.

Note

Random cases can step over one sharp edge.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

A property test fails. What does proptest report?