How do I use predicates for testing in Rust?

Walkthrough

The predicates crate provides a fluent, composable API for writing assertions and predicates in tests. Instead of writing custom assertion logic, you chain predicate functions to create expressive, readable test conditions. Predicates can be combined with AND/OR logic, negated, and used with various data types including strings, numbers, collections, and paths. The crate integrates well with the assert_cmd crate for testing CLI applications and produces detailed failure messages showing exactly what was expected vs. what was received.

Key concepts:

  1. Predicate trait — the core trait for boolean conditions
  2. Factory functionspredicate::eq(), predicate::str::contains(), etc.
  3. Combinators.and(), .or(), .not() for combining predicates
  4. eval() — evaluate predicate against a value
  5. Integration with assert_cmd — seamless CLI testing

Code Example

# Cargo.toml
[dev-dependencies]
predicates = "3"
use predicates::prelude::*;
 
fn main() {
    let predicate = predicate::eq(42);
    assert!(predicate.eval(&42));
}

Basic Predicates

use predicates::prelude::*;
 
fn main() {
    // Equality
    let is_42 = predicate::eq(42);
    assert!(is_42.eval(&42));
    assert!(!is_42.eval(&41));
    
    // Inequality
    let is_not_zero = predicate::ne(0);
    assert!(is_not_zero.eval(&1));
    assert!(!is_not_zero.eval(&0));
    
    // Greater than / Less than
    let is_positive = predicate::gt(0);
    assert!(is_positive.eval(&1));
    assert!(!is_positive.eval(&0));
    
    let in_range = predicate::ge(10).and(predicate::le(20));
    assert!(in_range.eval(&15));
    assert!(!in_range.eval(&5));
    
    // Floating point comparison
    let approx_pi = predicate::float::is_close(3.14159, 0.0001);
    assert!(approx_pi.eval(&3.14159));
}

String Predicates

use predicates::prelude::*;
 
fn main() {
    // Contains substring
    let contains_hello = predicate::str::contains("hello");
    assert!(contains_hello.eval("say hello world"));
    assert!(!contains_hello.eval("say hi world"));
    
    // Starts with / Ends with
    let starts_with = predicate::str::starts_with("prefix");
    assert!(starts_with.eval("prefix-suffix"));
    
    let ends_with = predicate::str::ends_with("suffix");
    assert!(ends_with.eval("prefix-suffix"));
    
    // Regex matching
    let is_email = predicate::str::is_match("^[a-z]+@[a-z]+\\.[a-z]+$").unwrap();
    assert!(is_email.eval("test@example.com"));
    assert!(!is_email.eval("invalid-email"));
    
    // String similarity (Levenshtein distance)
    let similar = predicate::str::similar("hello").distance(1);
    assert!(similar.eval("hallo"));
    assert!(!similar.eval("hi"));
    
    // Case-insensitive contains
    let contains_case = predicate::str::contains("hello").trim().case();
    // Note: case() for case-insensitive, trim() trims whitespace
    
    // Is empty
    let is_empty = predicate::str::is_empty();
    assert!(is_empty.eval(""));
    assert!(!is_empty.eval("not empty"));
}

Path Predicates

use predicates::prelude::*;
use std::path::Path;
 
fn main() {
    // Exists
    let exists = predicate::path::exists();
    // assert!(exists.eval(Path::new("/etc/passwd")));
    
    // Is file / directory
    let is_file = predicate::path::is_file();
    let is_dir = predicate::path::is_dir();
    
    // File extension
    let is_rust = predicate::path::extension().eq("rs");
    assert!(is_rust.eval(Path::new("main.rs")));
    assert!(!is_rust.eval(Path::new("main.txt")));
    
    // File name
    let is_cargo = predicate::path::file_name().eq("Cargo.toml");
    assert!(is_cargo.eval(Path::new("project/Cargo.toml")));
    
    // File content contains
    let contains_line = predicate::path::contains("fn main");
    // assert!(contains_line.eval(Path::new("main.rs")));
    
    // File size
    let is_small = predicate::path::file_size().lt(1024);
    // assert!(is_small.eval(Path::new("small_file.txt")));
}

Combining Predicates

use predicates::prelude::*;
 
fn main() {
    let is_even = predicate::function(|x: &i32| *x % 2 == 0);
    let is_positive = predicate::gt(0);
    
    // AND: both conditions must be true
    let positive_and_even = is_positive.and(is_even);
    assert!(positive_and_even.eval(&42));
    assert!(!positive_and_even.eval(&-2)); // not positive
    assert!(!positive_and_even.eval(&3)); // not even
    
    // OR: either condition can be true
    let is_zero_or_positive = predicate::eq(0).or(predicate::gt(0));
    assert!(is_zero_or_positive.eval(&0));
    assert!(is_zero_or_positive.eval(&5));
    assert!(!is_zero_or_positive.eval(&-5));
    
    // NOT: negate any predicate
    let is_negative = predicate::gt(0).not();
    assert!(is_negative.eval(&-5));
    assert!(!is_negative.eval(&5));
    
    // Complex combinations
    let is_valid = predicate::ge(1)
        .and(predicate::le(100))
        .and(is_even);
    assert!(is_valid.eval(&42));
    assert!(!is_valid.eval(&101));
}

Function Predicates

use predicates::prelude::*;
 
fn is_palindrome(s: &str) -> bool {
    let chars: Vec<char> = s.chars().collect();
    let reversed: Vec<char> = chars.iter().rev().copied().collect();
    chars == reversed
}
 
fn is_prime(n: &u32) -> bool {
    if *n < 2 { return false; }
    for i in 2..=(*n as f64).sqrt() as u32 {
        if *n % i == 0 { return false; }
    }
    true
}
 
fn main() {
    // Create predicate from function
    let palindrome = predicate::function(is_palindrome);
    assert!(palindrome.eval("radar"));
    assert!(!palindrome.eval("hello"));
    
    let prime = predicate::function(is_prime);
    assert!(prime.eval(&7));
    assert!(!prime.eval(&8));
    
    // Closure predicates
    let is_long = predicate::function(|s: &&str| s.len() > 5);
    assert!(is_long.eval("hello world"));
    assert!(!is_long.eval("hi"));
}

Using in Tests

// In lib.rs or tests/
use predicates::prelude::*;
 
fn process(input: &str) -> String {
    input.to_uppercase()
}
 
#[cfg(test)]
mod tests {
    use super::*;
    use predicates::prelude::*;
    
    #[test]
    fn test_process() {
        let result = process("hello");
        
        // Using assert with predicate
        assert!(predicate::str::contains("HELLO").eval(&result));
        
        // Using predicate's assert method for better error messages
        predicate::str::contains("HELLO").assert(&result);
    }
    
    #[test]
    fn test_output_format() {
        let result = process("test");
        
        // Chain multiple predicates
        let is_valid = predicate::str::contains("TEST")
            .and(predicate::str::starts_with("T"))
            .and(predicate::str::ends_with("T"));
        
        is_valid.assert(&result);
    }
    
    #[test]
    fn test_length() {
        let result = process("hello");
        
        // String length predicate
        let correct_length = predicate::str::diff("HELLO");
        correct_length.assert(&result);
    }
}
 
fn main() {
    // Run tests with: cargo test
}

Collection Predicates

use predicates::prelude::*;
 
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Contains element
    let has_three = predicate::iter::contains(3);
    assert!(has_three.eval(&numbers.iter().copied()));
    
    // Contains specific item
    let has_item = predicate::iter::contains(&3);
    assert!(has_item.eval(&numbers.iter()));
    
    // All elements satisfy predicate
    let all_positive = predicate::iter::all(predicate::gt(0));
    assert!(all_positive.eval(&numbers.iter().copied()));
    
    // Any element satisfies predicate
    let any_even = predicate::iter::any(predicate::function(|x: &i32| *x % 2 == 0));
    assert!(any_even.eval(&numbers.iter()));
    
    // Count predicates
    let exactly_three = predicate::iter::count(3);
    assert!(exactly_three.eval(&vec![1, 2, 3].iter()));
    
    let at_least_two = predicate::iter::count().gt(1);
    assert!(at_least_two.eval(&vec![1, 2, 3].iter()));
}

Float Predicates

use predicates::prelude::*;
 
fn main() {
    // Near equality with tolerance
    let near_pi = predicate::float::is_close(std::f64::consts::PI, 0.001);
    assert!(near_pi.eval(&3.1415));
    assert!(!near_pi.eval(&3.0));
    
    // In range
    let in_range = predicate::float::is_close(1.0, 0.1).or(
        predicate::float::is_close(2.0, 0.1)
    );
    assert!(in_range.eval(&1.05));
    assert!(in_range.eval(&1.95));
    
    // NaN and Infinity
    let is_nan = predicate::float::is_nan();
    assert!(is_nan.eval(&f64::NAN));
    
    let is_finite = predicate::float::is_finite();
    assert!(is_finite.eval(&42.0));
    assert!(!is_finite.eval(&f64::INFINITY));
}

Ord Predicates

use predicates::prelude::*;
 
fn main() {
    let value = 42;
    
    // Greater than
    assert!(predicate::gt(40).eval(&value));
    
    // Greater or equal
    assert!(predicate::ge(42).eval(&value));
    
    // Less than
    assert!(predicate::lt(50).eval(&value));
    
    // Less or equal
    assert!(predicate::le(42).eval(&value));
    
    // Range checks
    let in_range = predicate::ge(10).and(predicate::le(100));
    assert!(in_range.eval(&50));
    
    // Between (inclusive)
    let between = predicate::ge(1).and(predicate::le(10));
    assert!(between.eval(&5));
}

Boxed Predicates

use predicates::prelude::*;
 
fn main() {
    // Box predicates for storage or return types
    let boxed: Box<dyn Predicate<i32>> = Box::new(
        predicate::ge(0).and(predicate::le(100))
    );
    
    assert!(boxed.eval(&50));
    
    // Useful for functions returning predicates
    fn make_validator(min: i32, max: i32) -> Box<dyn Predicate<i32>> {
        Box::new(predicate::ge(min).and(predicate::le(max)))
    }
    
    let validator = make_validator(10, 20);
    assert!(validator.eval(&15));
}

String Difference Assertions

use predicates::prelude::*;
 
fn main() {
    let expected = "Hello, World!";
    let actual = "Hello, World!";
    
    // Exact match
    predicate::str::diff(expected).assert(actual);
    
    // Regex match with capture
    let is_date = predicate::str::is_match("^\d{4}-\d{2}-\d{2}$").unwrap();
    assert!(is_date.eval("2024-01-15"));
    
    // String similarity
    let similar = predicate::str::similar("hello").distance(2);
    assert!(similar.eval("hallo")); // 1 char different
    assert!(similar.eval("hllo"));  // 1 char missing
    assert!(!similar.eval("hi"));   // too different
}

Integration with assert_cmd

// Cargo.toml
// [dev-dependencies]
// assert_cmd = "2"
// predicates = "3"
 
use assert_cmd::Command;
use predicates::prelude::*;
 
fn main() {
    // This would typically be in a #[test] function
    // Example of testing a CLI application
    
    /*
    let mut cmd = Command::cargo_bin("my_app").unwrap();
    cmd.arg("--version");
    
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("my_app 1.0"));
    
    // Test error case
    let mut cmd = Command::cargo_bin("my_app").unwrap();
    cmd.arg("--invalid-flag");
    
    cmd.assert()
        .failure()
        .stderr(predicate::str::contains("error").or(
               predicate::str::contains("unknown option")
        ));
    */
    
    println!("See assert_cmd crate for CLI testing");
}

Testing File Output

use predicates::prelude::*;
use std::io::Write;
use tempfile::NamedTempFile;
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create temp file
    let mut temp_file = NamedTempFile::new()?;
    writeln!(temp_file, "Hello, World!")?;
    writeln!(temp_file, "Line 2")?;
    temp_file.flush()?;
    
    let path = temp_file.path();
    
    // Test file exists
    assert!(predicate::path::exists().eval(path));
    
    // Test is file (not directory)
    assert!(predicate::path::is_file().eval(path));
    
    // Test file content
    let contains_hello = predicate::path::contains("Hello");
    assert!(contains_hello.eval(path));
    
    // Test file extension
    // Note: temp files may not have extensions
    
    Ok(())
}

Custom Predicate Types

use predicates::prelude::*;
use std::fmt;
 
// Custom predicate for validation
struct ValidEmail;
 
impl Predicate<&str> for ValidEmail {
    fn eval(&self, input: &&str) -> bool {
        input.contains('@') && input.contains('.')
    }
}
 
impl predicates::reflection::PredicateReflection for ValidEmail {}
 
impl fmt::Display for ValidEmail {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "valid_email")
    }
}
 
fn main() {
    let is_email = ValidEmail;
    
    assert!(is_email.eval(&"test@example.com"));
    assert!(!is_email.eval(&"invalid"));
    
    // Use with assert
    is_email.assert("user@domain.org");
}

Testing JSON Output

use predicates::prelude::*;
 
fn main() {
    let json_output = r#"{"name": "Alice", "age": 30}"#;
    
    // Contains key
    let has_name = predicate::str::contains("\"name\"");
    assert!(has_name.eval(json_output));
    
    // Contains value
    let has_alice = predicate::str::contains("Alice");
    assert!(has_alice.eval(json_output));
    
    // Regex for JSON pattern
    let has_age_field = predicate::str::is_match("\"age\":\s*\d+").unwrap();
    assert!(has_age_field.eval(json_output));
}

Async Testing Pattern

// Note: predicates doesn't directly support async, but you can
// evaluate async results synchronously
use predicates::prelude::*;
 
async fn fetch_data() -> String {
    // Simulated async operation
    "data from server".to_string()
}
 
fn main() {
    // In a real test, use tokio::test
    let rt = tokio::runtime::Runtime::new().unwrap();
    let result = rt.block_on(fetch_data());
    
    let is_valid = predicate::str::contains("data")
        .and(predicate::str::ends_with("server"));
    
    assert!(is_valid.eval(&result));
}

Negation and Edge Cases

use predicates::prelude::*;
 
fn main() {
    // Negate any predicate
    let not_zero = predicate::eq(0).not();
    assert!(not_zero.eval(&1));
    assert!(!not_zero.eval(&0));
    
    // Not empty string
    let not_empty = predicate::str::is_empty().not();
    assert!(not_empty.eval("hello"));
    
    // Not contains
    let not_admin = predicate::str::contains("admin").not();
    assert!(not_admin.eval("user_role"));
    
    // Complex: not (A and B) = not A or not B
    let not_both = predicate::str::contains("a")
        .and(predicate::str::contains("b"))
        .not();
    assert!(not_both.eval("a only"));  // has 'a' but not 'b'
    assert!(not_both.eval("b only"));  // has 'b' but not 'a'
}

Debug Output on Failure

use predicates::prelude::*;
 
fn main() {
    let result = "Hello, World!";
    
    // When this fails, it shows exactly what was expected
    // predicate::str::starts_with("Hola").assert(result);
    // Output:
    // assertion failed: `Hello, World!` does not start with `Hola`
    
    // Instead use pass() for more info
    let pred = predicate::str::starts_with("Hello");
    if pred.eval(result) {
        println!("Predicate passed!");
    }
    
    // Find() gives detailed info
    let pred = predicate::str::contains("World");
    if let Some(case) = pred.find_case(true, result) {
        println!("Found: {}", case.tree());
    }
}

Testing Error Messages

use predicates::prelude::*;
use std::error::Error;
use std::fmt;
 
#[derive(Debug)]
struct MyError {
    message: String,
}
 
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "MyError: {}", self.message)
    }
}
 
impl Error for MyError {}
 
fn validate(input: i32) -> Result<i32, MyError> {
    if input < 0 {
        Err(MyError { message: "negative value".to_string() })
    } else {
        Ok(input)
    }
}
 
fn main() {
    // Test error case
    let error = validate(-1).unwrap_err();
    let error_msg = error.to_string();
    
    assert!(predicate::str::contains("negative").eval(&error_msg));
    
    // Test success case
    let result = validate(42).unwrap();
    assert!(predicate::eq(42).eval(&result));
}

Boolean Logic Table

use predicates::prelude::*;
 
fn main() {
    let a = predicate::eq(true);
    let b = predicate::eq(true);
    
    // AND truth table
    println!("AND: {:?}", a.and(b).eval(&true));  // true
    
    // OR truth table
    let a = predicate::eq(true);
    let b = predicate::eq(false);
    println!("OR: {:?}", a.or(b).eval(&true));   // true
    
    // XOR-like (one but not both)
    let a = predicate::eq(true);
    let b = predicate::eq(true);
    let xor = a.and(b.not()).or(a.not().and(b));
    println!("XOR: {:?}", xor.eval(&true)); // false (both true)
}

Real-World Test Example

// tests/integration_test.rs
use predicates::prelude::*;
 
struct Calculator {
    result: i32,
}
 
impl Calculator {
    fn new() -> Self {
        Self { result: 0 }
    }
    
    fn add(&mut self, n: i32) -> &mut Self {
        self.result += n;
        self
    }
    
    fn subtract(&mut self, n: i32) -> &mut Self {
        self.result -= n;
        self
    }
    
    fn multiply(&mut self, n: i32) -> &mut Self {
        self.result *= n;
        self
    }
    
    fn get_result(&self) -> i32 {
        self.result
    }
}
 
#[cfg(test)]
mod tests {
    use super::*;
    use predicates::prelude::*;
    
    #[test]
    fn test_basic_operations() {
        let mut calc = Calculator::new();
        
        calc.add(10)
            .subtract(3)
            .multiply(2);
        
        // (0 + 10 - 3) * 2 = 14
        assert!(predicate::eq(14).eval(&calc.get_result()));
    }
    
    #[test]
    fn test_result_range() {
        let mut calc = Calculator::new();
        calc.add(50);
        
        let result = calc.get_result();
        
        // Result should be positive and less than 100
        let in_range = predicate::gt(0).and(predicate::lt(100));
        assert!(in_range.eval(&result));
    }
}
 
fn main() {
    let mut calc = Calculator::new();
    calc.add(5).subtract(3);
    
    let is_positive = predicate::gt(0);
    assert!(is_positive.eval(&calc.get_result()));
    
    println!("Result: {}", calc.get_result());
}

Summary

  • predicate::eq(value) — equality check
  • predicate::gt/ge/lt/le(value) — comparison checks
  • predicate::str::contains()/starts_with()/ends_with() — string predicates
  • predicate::str::is_match(pattern) — regex matching
  • predicate::path::exists()/is_file()/is_dir() — file system predicates
  • .and(), .or(), .not() — combine predicates
  • predicate::function(|x| ...) — custom predicate from function
  • predicate::iter::contains()/all()/any() — collection predicates
  • predicate::float::is_close(expected, tolerance) — float comparison
  • .eval(&value) — returns true/false
  • .assert(&value) — panics with detailed message on failure
  • Works great with assert_cmd for CLI testing
  • Provides clear, readable test output