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

Recipes

Shorter answers to common questions, each independent of the others.

Assert several things about one value

all_of combines matchers: the value must satisfy every one. any_of is the or-form. Both take a tuple of matchers:

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

#[test]
fn the_score_is_in_a_sensible_range() -> TestResult {
    let score = 73_u32;
    check!(score).satisfies(all_of((ge(0), le(100), ne(50))))?;
    Ok(())
}
}

Keep going after the first failure: soft

A ? on a failed check! returns immediately, so a test stops at its first failure. When you want to see every failure in one run (checking each field of a response, say), soft collects them:

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

#[test]
fn every_field_is_checked() -> TestResult {
    soft(|s| {
        s.check(&1, eq(1));
        s.check(&"alice", eq("alice"));
        s.check(&true, is_true());
    })
}
}

soft returns Ok(()) if every soft assertion passed, or a single TestError that renders all of them, each with its own source location. Inside the closure, s.check(&value, matcher) is the soft form of check!, and s.context("...") opens a labeled scope for the assertions that follow.

Match the shape of a struct, tuple, or enum

The structural macros assert on shape without a custom matcher. Each field position holds a matcher, and .. ignores the rest:

#![allow(unused)]
fn main() {
use test_better::prelude::*;
use test_better::{matches_struct, matches_tuple, matches_variant};

#[derive(Debug)]
struct User { name: String, age: u32, email: String }
#[derive(Debug)]
struct Point(i32, i32);
#[derive(Debug)]
enum Shape { Circle { radius: f64 } }
#[test]
fn structural_matchers() -> TestResult {
    let user = User { name: "alice".into(), age: 30, email: "alice@example.com".into() };
    check!(user).satisfies(matches_struct!(User {
        name: eq(String::from("alice")),
        age: gt(18u32),
        ..
    }))?;

    check!(Point(3, 4)).satisfies(matches_tuple!(Point(gt(0), lt(100))))?;

    check!(Shape::Circle { radius: 2.0 })
        .satisfies(matches_variant!(Shape::Circle { radius: gt(0.0) }))?;
    Ok(())
}
}

On a failure, the message names the field or position that did not match. The matchers nest: an inner matches_struct! is just another matcher expression.

Assert on collections

contains takes a matcher and checks at least one element satisfies it; every checks all of them; have_len, is_empty, and is_not_empty check size. contains_in_order checks a subsequence:

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

#[test]
fn collection_matchers() -> TestResult {
    let scores = vec![10, 20, 30, 40];
    check!(&scores).satisfies(have_len(4))?;
    check!(&scores).satisfies(contains(eq(30)))?;
    check!(&scores).satisfies(every(gt(0)))?;
    check!(&scores).satisfies(contains_in_order([eq(10), eq(40)]))?;
    Ok(())
}
}

Parameterized tests with #[test_case]

#[test_case] turns one function into many generated #[test]s, one per attribute line. Each line is the argument list, optionally followed by ; "label":

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

#[test_case(2, 2, 4)]
#[test_case(10, 5, 15 ; "bigger numbers")]
fn addition_works(a: i32, b: i32, sum: i32) -> TestResult {
    check!(a + b).satisfies(eq(sum))
}
}

The generated tests are gathered into a module named for the function, so the second case above runs as addition_works::bigger_numbers. Import test_case by name: it is deliberately kept out of the prelude because std exports an attribute of the same name.

Match a string

contains_str, starts_with, and ends_with are the substring matchers; with the regex feature, matches_regex takes a pattern:

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

#[test]
fn string_matchers() -> TestResult {
    let greeting = "Hello, alice!";
    check!(greeting).satisfies(starts_with("Hello"))?;
    check!(greeting).satisfies(contains_str("alice"))?;
    check!(greeting).satisfies(ends_with("!"))?;
    Ok(())
}
}

The cargo test-better runner

test-better-runner provides an optional cargo-test-better binary: a thin wrapper around cargo test that groups failures by their context area and prints a run summary. Install it and run it in place of cargo test:

cargo install test-better-runner
cargo test-better

It is opt-in tooling: your tests do not depend on it, and a plain cargo test behaves exactly as before. The same crate’s cargo test-better accept subcommand applies the pending patches that inline snapshots record under UPDATE_SNAPSHOTS=1.

Control colored output

Failure rendering is colored when the output is a terminal. To force it on or off (in CI logs, or when capturing output for a test), set the color choice:

use test_better::{ColorChoice, set_color_choice};

fn main() {
set_color_choice(ColorChoice::Never);
}

ColorChoice is Always, Never, or Auto; color_choice() reads the current setting.

Inspect a failure as data

For tooling, TestError::to_structured() produces a StructuredError: an owned, Clone-able, serde-serializable (behind the serde feature) form of the failure, with the kind, message, location, context chain, and payload. It is what the cargo-test-better runner consumes; a test that needs to assert on the structure of a failure rather than its rendered text can use it directly.