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:
- Predicate trait — the core trait for boolean conditions
- Factory functions —
predicate::eq(),predicate::str::contains(), etc. - Combinators —
.and(),.or(),.not()for combining predicates - eval() — evaluate predicate against a value
- 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 checkpredicate::gt/ge/lt/le(value)— comparison checkspredicate::str::contains()/starts_with()/ends_with()— string predicatespredicate::str::is_match(pattern)— regex matchingpredicate::path::exists()/is_file()/is_dir()— file system predicates.and(),.or(),.not()— combine predicatespredicate::function(|x| ...)— custom predicate from functionpredicate::iter::contains()/all()/any()— collection predicatespredicate::float::is_close(expected, tolerance)— float comparison.eval(&value)— returns true/false.assert(&value)— panics with detailed message on failure- Works great with
assert_cmdfor CLI testing - Provides clear, readable test output
