How can predicates crate improve readability of assertions in tests compared to plain assert! macros?

The predicates crate provides a functional, composable approach to writing assertions that reads naturally and produces clear failure messages. Unlike raw assert! macros that require manual condition construction and error messages, predicates are self-documenting and automatically generate informative output when tests fail.

The Problem with Plain Assertions

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_with_plain_assert() {
        let result = calculate_something();
        
        // Basic assertions are simple enough
        assert!(result.success);
        
        // But complex conditions become hard to read
        assert!(result.value > 0 && result.value < 100);
        
        // And failure messages are often unhelpful
        assert!(result.success, "Expected success but got failure");
        
        // You end up writing verbose messages
        assert!(
            result.value > 0,
            "Expected value to be positive, but got {}",
            result.value
        );
        
        // Comparisons require manual message construction
        assert_eq!(result.count, 5, "Expected count to be 5");
    }
}

Plain assert! requires you to write both the condition and the error message.

Basic Predicates Usage

use predicates::prelude::*;
 
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_with_predicates() {
        let result = calculate_something();
        
        // Simple predicates read naturally
        assert_that!(result.success, predicates::bool::is_true());
        
        // Value is automatically shown on failure
        assert_that!(result.value, predicates::num::gt(0));
        assert_that!(result.value, predicates::num::lt(100));
        
        // Multiple assertions can be clearer
        assert_that!(result.count, predicates::ord::eq(5));
    }
}

Predicates express what you're checking, and failures explain what went wrong.

Installation and Imports

# Cargo.toml
[dev-dependencies]
predicates = "3"
// The prelude brings in common traits and macros
use predicates::prelude::*;
 
// Or import specific modules
use predicates::{boolean::PredicateBooleanExt, num, str};

Numeric Predicates

use predicates::prelude::*;
 
#[test]
fn numeric_predicates() {
    let value = 42;
    
    // Comparison predicates
    assert_that!(value, predicates::gt(40));     // greater than
    assert_that!(value, predicates::lt(50));     // less than
    assert_that!(value, predicates::ge(42));     // greater or equal
    assert_that!(value, predicates::le(42));     // less or equal
    assert_that!(value, predicates::eq(42));     // equal
    
    // These produce clear error messages:
    // assertion failed: `42 gt 50`
    // expected: 50
    // actual: 42
}

Numeric predicates make comparisons self-documenting.

String Predicates

use predicates::prelude::*;
 
#[test]
fn string_predicates() {
    let text = "Hello, World!";
    
    // Exact match
    assert_that!(text, predicates::str::is_empty().not());
    
    // Contains substring
    assert_that!(text, predicates::str::contains("World"));
    
    // Starts with / ends with
    assert_that!(text, predicates::str::starts_with("Hello"));
    assert_that!(text, predicates::str::ends_with("!"));
    
    // Pattern matching
    assert_that!(text, predicates::str::is_match(r"Hello, \w+!").unwrap());
    
    // Case-insensitive comparison
    assert_that!(text, predicates::str::similar("hello, world!"));
}

String predicates provide expressive matching without regex complexity.

Combining Predicates

use predicates::prelude::*;
 
#[test]
fn combining_predicates() {
    let value = 42;
    
    // And: both must be true
    assert_that!(
        value,
        predicates::gt(0).and(predicates::lt(100))
    );
    
    // Or: at least one must be true
    assert_that!(
        value,
        predicates::eq(42).or(predicates::eq(0))
    );
    
    // Not: negate a predicate
    assert_that!(value, predicates::eq(0).not());
    
    // Complex combinations
    assert_that!(
        value,
        predicates::gt(0)
            .and(predicates::lt(50))
            .and(predicates::eq(42).not())
    );
}

Predicates compose naturally with and, or, and not.

Predicate Combinators

use predicates::prelude::*;
 
#[test]
fn combinator_examples() {
    let value = 42;
    
    // Chain multiple conditions
    let in_range = predicates::gt(0).and(predicates::lt(100));
    assert_that!(value, in_range);
    
    // Store predicates for reuse
    let is_valid = predicates::gt(0)
        .and(predicates::lt(100))
        .and(predicates::ne(50));  // not exactly 50
    
    assert_that!(value, is_valid);
    
    // Use in multiple tests
    assert_that!(55, is_valid);
    assert_that!(25, is_valid);
}

Predicates can be stored and reused across tests.

Collection Predicates

use predicates::prelude::*;
 
#[test]
fn collection_predicates() {
    let items = vec![1, 2, 3, 4, 5];
    
    // Check if collection contains element
    assert_that!(items.clone(), predicates::iter::contains(3));
    
    // Check if collection contains a specific element
    assert_that!(&items, predicates::iter::contains(&3));
    
    // Check collection properties
    assert_that!(items.clone(), predicates::iter::has_count(5));
    
    // Check if all elements match a predicate
    assert_that!(
        items.clone(),
        predicates::iter::all(predicates::gt(0))
    );
    
    // Check if any element matches
    assert_that!(
        items.clone(),
        predicates::iter::any(predicates::eq(5))
    );
}

Collection predicates work with any iterable.

Option and Result Predicates

use predicates::prelude::*;
 
#[test]
fn option_predicates() {
    let some_value: Option<i32> = Some(42);
    let none_value: Option<i32> = None;
    
    // Check if Some
    assert_that!(some_value, predicates::option::some(predicates::gt(40)));
    
    // Check if None
    assert_that!(none_value, predicates::option::none());
    
    // Check the contained value
    assert_that!(some_value, predicates::option::some(predicates::eq(42)));
}
 
#[test]
fn result_predicates() {
    let ok_value: Result<i32, &str> = Ok(42);
    let err_value: Result<i32, &str> = Err("failed");
    
    // Check if Ok
    assert_that!(ok_value, predicates::result::ok(predicates::eq(42)));
    
    // Check if Err
    assert_that!(err_value, predicates::result::err(predicates::eq("failed")));
}

Option and Result predicates check both the variant and contained value.

Path Predicates

use predicates::prelude::*;
use std::path::Path;
 
#[test]
fn path_predicates() {
    let path = Path::new("/some/path/to/file.txt");
    
    // Check existence
    assert_that!(path, predicates::path::exists());
    
    // Check file properties
    assert_that!(path, predicates::path::is_file());
    
    // Check extension
    assert_that!(path, predicates::path::has_extension("txt"));
    
    // Check basename
    assert_that!(path, predicates::str::ends_with("file.txt"));
}

Path predicates are useful for filesystem-related tests.

Custom Predicates

use predicates::prelude::*;
use predicates::Predicate;
 
// Define a custom predicate using a closure
fn is_even() -> impl Predicate<i32> {
    predicates::function::function(|x: &i32| *x % 2 == 0)
        .fn_name("is_even")
}
 
#[test]
fn custom_predicate() {
    assert_that!(4, is_even());
    assert_that!(6, is_even());
    
    // This would fail with a clear message:
    // assert_that!(5, is_even());
    // assertion failed: `is_even`
    // actual: 5
}

Custom predicates encapsulate complex logic.

Float Predicates

use predicates::prelude::*;
 
#[test]
fn float_predicates() {
    let value = 3.14159;
    
    // Approximate equality for floats
    assert_that!(value, predicates::float::close_to(3.14, 0.01));
    
    // Not close to
    assert_that!(value, predicates::float::close_to(3.0, 0.01).not());
    
    // Handle edge cases
    assert_that!(f64::INFINITY, predicates::float::is_infinite());
    assert_that!(f64::NAN, predicates::float::is_nan());
}

Float predicates handle floating-point comparison properly.

Comparison with Plain Assert

use predicates::prelude::*;
 
#[test]
fn comparison_plain_vs_predicates() {
    let result = Some(42);
    
    // Plain assert - verbose and unclear on failure
    assert!(
        result.is_some() && result.unwrap() > 40,
        "Expected Some(value > 40), got {:?}",
        result
    );
    
    // Predicates - clear and self-documenting
    assert_that!(
        result,
        predicates::option::some(predicates::gt(40))
    );
    
    // Failure message from predicates:
    // assertion failed
    // expected: Some(greater than 40)
    // actual: Some(35)
}

Predicates produce better error messages automatically.

Debug Output on Failure

use predicates::prelude::*;
 
#[test]
fn debug_output() {
    let value = 42;
    
    // When this assertion fails:
    assert_that!(value, predicates::gt(100));
    
    // You get clear output:
    // assertion failed
    // expected: greater than 100
    // actual: 42
    
    let text = "hello";
    assert_that!(text, predicates::str::starts_with("world"));
    
    // Output:
    // assertion failed
    // expected: starts with "world"
    // actual: "hello"
}

Failure messages explain both what was expected and what was found.

Integration with Test Frameworks

use predicates::prelude::*;
 
// Works with any test framework that supports panics on failure
 
#[test]
fn standard_test_framework() {
    // Works with #[test]
    assert_that!(42, predicates::gt(0));
}
 
// Works with cargo test output
#[test]
#[should_panic(expected = "assertion failed")]
fn expected_failure() {
    assert_that!(0, predicates::gt(0));
}

Predicates work with Rust's built-in test framework.

Readable Test Documentation

use predicates::prelude::*;
 
#[test]
fn user_validation() {
    let user = create_user("alice", "alice@example.com");
    
    // Predicates read like documentation
    assert_that!(user.username, predicates::str::starts_with("a"));
    assert_that!(user.email, predicates::str::contains("@"));
    assert_that!(user.age, predicates::gt(0).and(predicates::lt(150)));
    assert_that!(user.active, predicates::bool::is_true());
    
    // Compare to plain asserts:
    // assert!(user.username.starts_with("a"));
    // assert!(user.email.contains("@"));
    // assert!(user.age > 0 && user.age < 150);
    // assert!(user.active);
}

Predicate-based tests serve as readable documentation.

Transforming Values

use predicates::prelude::*;
 
#[test]
fn transforming_values() {
    let result = "  Hello, World!  ";
    
    // Map the value before testing
    assert_that!(
        result,
        predicates::str::trim().eq("Hello, World!")
    );
    
    // Compare to plain assert:
    // assert_eq!(result.trim(), "Hello, World!");
}

Transformations allow testing derived values.

Boolean Predicates

use predicates::prelude::*;
 
#[test]
fn boolean_predicates() {
    let flag = true;
    
    // Direct boolean check
    assert_that!(flag, predicates::bool::is_true());
    assert_that!(flag, predicates::bool::is_false().not());
    
    // More readable than:
    // assert!(flag);
    // assert!(!flag);
}

Boolean predicates are explicit about expectations.

Structured Assertions

use predicates::prelude::*;
 
struct User {
    name: String,
    age: u32,
    email: String,
}
 
#[test]
fn structured_assertions() {
    let user = User {
        name: "Alice".to_string(),
        age: 30,
        email: "alice@example.com".to_string(),
    };
    
    // Instead of:
    // assert_eq!(user.name, "Alice");
    // assert!(user.age > 18);
    // assert!(user.email.contains("@"));
    
    // Use:
    assert_that!(&user.name, predicates::eq("Alice"));
    assert_that!(user.age, predicates::gt(18));
    assert_that!(&user.email, predicates::str::contains("@"));
}

Predicates make structured assertions consistent.

Synthesis

The predicates crate improves test readability and maintainability through:

Composability:

// Complex conditions become readable chains
assert_that!(value, predicates::gt(0).and(predicates::lt(100)));

Self-documenting assertions:

// Intent is clear from the predicate name
assert_that!(email, predicates::str::contains("@"));

Automatic error messages:

// Failures explain what was expected vs actual
// assertion failed
// expected: contains "@"
// actual: "invalid-email"

Reusability:

// Predicates can be stored and reused
let is_valid = predicates::gt(0).and(predicates::lt(100));
assert_that!(value, is_valid);

Key comparison:

Plain assert! predicates
assert!(x > 0) assert_that!(x, predicates::gt(0))
assert!(x > 0 && x < 100) assert_that!(x, predicates::gt(0).and(predicates::lt(100)))
assert!(s.contains("x")) assert_that!(s, predicates::str::contains("x"))
Manual error messages Automatic descriptions

Use predicates when test readability matters, especially for complex conditions or when you want clear failure messages without manual message construction.