Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Property Testing

A property test asserts that something holds for every input in a range, rather than for a handful of hand-picked cases. test-better’s property layer is a thin seam over proptest: you write the property as a closure that returns TestResult, and a failure is shrunk to a minimal counterexample that still carries the matcher failure that broke it.

The property! macro

The everyday form is the property! macro. The closure binding’s type names the strategy: any type that is proptest::Arbitrary (most std types are) is inferred from the annotation.

#![allow(unused)]
fn main() {
use test_better::prelude::*;

#[test]
fn incrementing_changes_the_value() -> TestResult {
    property!(|n: u32| {
        check!(n.wrapping_add(1)).satisfies(ne(n))
    })
}
}

The macro call is the test body: it returns the TestResult the #[test] function returns.

To name an explicit strategy instead of inferring one, add a using clause. The binding is then bare; its type and values come from the strategy. A numeric range is a proptest strategy, so it works directly:

#![allow(unused)]
fn main() {
use test_better::prelude::*;

#[test]
fn values_in_range_stay_in_range() -> TestResult {
    property!(|n| {
        check!(n).satisfies(lt(10u64))
    } using 0u64..10)
}
}

Shrinking and counterexamples

When a property fails, proptest shrinks the failing input toward the simplest value that still fails, and test-better reports both the shrunk and the original input, alongside the matcher failure:

#![allow(unused)]
fn main() {
use test_better::prelude::*;

#[test]
fn this_property_is_false() -> TestResult {
    // "every value in 0..1000 is below 500" is false; the run shrinks the
    // counterexample down to exactly 500.
    let error = property!(|n: u32| {
        check!(n).satisfies(lt(500u32))
    } using 0u32..1_000)
    .err()
    .or_fail_with("values at or above 500 exist in 0..1000")?;

    let rendered = error.to_string();
    check!(rendered.contains("the shrunk (minimal) input is 500")).satisfies(is_true())?;
    check!(rendered.contains("less than 500")).satisfies(is_true())
}
}

The point of carrying the matcher failure through shrinking is that the report is not just “500 failed”: it is the full check! failure for the minimal input, so you see what about 500 broke the property.

The function form: for_all and for_all_with

property! expands to a call to for_all. You can call it directly when you want the Result<(), PropertyFailure<T>> as a value rather than as the test’s return:

#![allow(unused)]
fn main() {
use test_better::prelude::*;
use test_better::for_all;

#[test]
fn doubling_stays_in_bounds() -> TestResult {
    let outcome = for_all(0u32..1_000, |n| check!(n * 2).satisfies(lt(2_000u32)));
    check!(outcome.is_ok()).satisfies(is_true())
}
}

PropertyFailure<T> exposes the shrunk and original inputs and the carried failure: TestError, so a test can assert on the counterexample itself.

for_all_with takes a PropertyConfig (the case count) and a Runner (seeded deterministically or randomized), for when the defaults are not what you want:

#![allow(unused)]
fn main() {
use test_better::prelude::*;
use test_better::{PropertyConfig, Runner, for_all_with};

#[test]
fn run_more_cases() -> TestResult {
    let mut runner = Runner::randomized();
    let outcome = for_all_with(PropertyConfig { cases: 32 }, &mut runner, 0u64..10, |n| {
        check!(n).satisfies(lt(10u64))
    });
    check!(outcome.is_ok()).satisfies(is_true())
}
}

Custom strategies

A Strategy<T> describes how to generate and shrink values of T. Any proptest strategy is a test-better Strategy through a blanket impl, so proptest’s combinators (prop_map, prop_filter, tuples, collections) are available with no wrapper. any::<T>() is the default strategy for a type, the same one property! infers.

There is also an optional quickcheck bridge behind the quickcheck feature: arbitrary::<T>() turns a quickcheck::Arbitrary type into a Strategy<T>. proptest is the primary backend; reach for the bridge only when you already have quickcheck::Arbitrary impls you want to reuse.