3 unstable releases
Uses new Rust 2024
new 0.2.0 | Mar 30, 2025 |
---|---|
0.1.1 | Mar 30, 2025 |
0.1.0 | Mar 30, 2025 |
#166 in Testing
55 downloads per month
60KB
897 lines
FluentTest
A fluent, Jest-like testing library for Rust that builds upon the standard testing infrastructure. FluentTest provides expressive assertions with readable error messages while maintaining compatibility with Rust's built-in testing functionality.
Features
- Fluent, Expressive API: Write tests in a readable, chainable style similar to Jest.
- Helpful Error Messages: Get clear error messages that include variable names and expressions.
- Seamless Integration: Works alongside Rust's standard testing infrastructure.
- Beautiful Test Output: Enhanced test reporting with visual cues and better organization.
- Type-Safe Assertions: Leverages Rust's type system for compile-time safety.
Quick Start
Add FluentTest to your project:
cargo add fluent-test --dev
Write your first test:
use fluent_test::prelude::*;
#[test]
fn should_check_values() {
let my_number = 5;
let my_string = "hello world";
let my_vec = vec![1, 2, 3];
expect!(my_number).to_be_greater_than(3);
expect!(my_string).to_contain("world");
expect!(my_vec).to_have_length(3);
}
Available Matchers
FluentTest provides a comprehensive set of matchers for various types. All matchers support negation through either the
not()
method or the expect_not!
macro.
Boolean
- to_be_true - Checks if a boolean is true
- to_be_false - Checks if a boolean is false
Equality
- to_equal - Checks if a value equals another value
Numeric
- to_be_greater_than - Checks if a number is greater than another
- to_be_less_than - Checks if a number is less than another
- to_be_even - Checks if a number is even
- to_be_odd - Checks if a number is odd
- to_be_divisible_by - Checks if a number is divisible by another
- to_be_positive - Checks if a number is positive
- to_be_negative - Checks if a number is negative
- to_be_in_range - Checks if a number is within a specified range
String
- to_be_empty - Checks if a string is empty
- to_contain - Checks if a string contains a substring
- to_start_with - Checks if a string starts with a prefix
- to_end_with - Checks if a string ends with a suffix
- to_match_regex - Checks if a string matches a regex pattern
- to_have_length - Checks if a string has a specific length
- to_have_length_greater_than - Checks if a string length is greater than a value
- to_have_length_less_than - Checks if a string length is less than a value
Collection
- to_be_empty_collection - Checks if a collection is empty
- to_have_length_collection - Checks if a collection has a specific length
- to_contain_collection - Checks if a collection contains a specific element
- to_contain_all_of - Checks if a collection contains all specified elements
- to_equal_collection - Compares two collections for element-wise equality
HashMap
- to_be_empty_hashmap - Checks if a HashMap is empty
- to_have_length_hashmap - Checks if a HashMap has a specific length
- to_contain_key - Checks if a HashMap contains a specific key
- to_contain_entry - Checks if a HashMap contains a specific key-value pair
Option
- to_be_some - Checks if an Option contains a value
- to_be_none - Checks if an Option is None
- to_contain_value - Checks if an Option contains a specific value
Result
- to_be_ok - Checks if a Result is Ok
- to_be_err - Checks if a Result is Err
- to_contain_ok - Checks if a Result contains a specific Ok value
- to_contain_err - Checks if a Result contains a specific Err value
Using Not Modifiers
FluentTest supports two ways to negate expectations:
#[test]
fn test_not_modifiers() {
let value = 42;
let name = "Arthur";
// Two ways to use negated expectations
// 1. Using the .not() method (fluent API)
expect!(value).not().to_equal(100);
expect!(name).not().to_contain("Bob");
// 2. Using the expect_not! macro
expect_not!(value).to_equal(100);
}
Matcher Documentation
Boolean Matchers
to_be_true
Checks if a boolean is true.
fn test_boolean_true() {
let is_enabled = true;
let is_disabled = false;
expect!(is_enabled).to_be_true(); // Passes
expect!(is_disabled).not().to_be_true(); // Passes
}
to_be_false
Checks if a boolean is false.
fn test_boolean_false() {
let is_enabled = true;
let is_disabled = false;
expect!(is_disabled).to_be_false(); // Passes
expect!(is_enabled).not().to_be_false(); // Passes
}
Equality Matchers
to_equal
Checks if a value equals another value.
fn test_equality() {
let value = 42;
let name = "Arthur";
expect!(value).to_equal(42); // Passes
expect!(value).not().to_equal(13); // Passes
expect!(name).to_equal("Arthur"); // Passes
}
Numeric Matchers
to_be_greater_than
Checks if a number is greater than another number.
fn test_greater_than() {
let value = 42;
expect!(value).to_be_greater_than(30); // Passes
expect!(value).not().to_be_greater_than(50); // Passes
}
to_be_less_than
Checks if a number is less than another number.
fn test_less_than() {
let value = 42;
expect!(value).to_be_less_than(50); // Passes
expect!(value).not().to_be_less_than(30); // Passes
}
to_be_even
Checks if a number is even.
fn test_even() {
let even = 42;
let odd = 43;
expect!(even).to_be_even(); // Passes
expect!(odd).not().to_be_even(); // Passes
}
to_be_odd
Checks if a number is odd.
fn test_odd() {
let even = 42;
let odd = 43;
expect!(odd).to_be_odd(); // Passes
expect!(even).not().to_be_odd(); // Passes
}
to_be_divisible_by
Checks if a number is divisible by another number.
fn test_divisible_by() {
let value = 42;
expect!(value).to_be_divisible_by(7); // Passes (42 is divisible by 7)
expect!(value).not().to_be_divisible_by(5); // Not divisible by 5
}
to_be_positive
Checks if a number is positive (greater than zero).
fn test_positive() {
let positive = 42;
let negative = -42;
expect!(positive).to_be_positive(); // Passes
expect!(negative).not().to_be_positive(); // Passes
}
to_be_negative
Checks if a number is negative (less than zero).
fn test_negative() {
let positive = 42;
let negative = -42;
expect!(negative).to_be_negative(); // Passes
expect!(positive).not().to_be_negative(); // Passes
}
to_be_in_range
Checks if a number is within a specified range.
fn test_in_range() {
let value = 42;
expect!(value).to_be_in_range(40..=45); // Inclusive range 40 to 45
expect!(value).not().to_be_in_range(50..60); // Not in range 50 to 60
// Different range types
expect!(value).to_be_in_range(40..46); // Half-open range
expect!(value).to_be_in_range(30..); // Range from 30 upwards
expect!(value).to_be_in_range(..50); // Range up to but not including 50
}
String Matchers
to_be_empty
Checks if a string is empty.
fn test_empty_string() {
let empty = "";
let not_empty = "hello";
expect!(empty).to_be_empty(); // Passes
expect!(not_empty).not().to_be_empty(); // Passes
}
to_contain
Checks if a string contains a specified substring.
fn test_string_contains() {
let greeting = "Hello, world!";
expect!(greeting).to_contain("world"); // Passes
expect!(greeting).not().to_contain("moon"); // Passes
}
to_start_with
Checks if a string starts with a specified prefix.
fn test_string_starts_with() {
let greeting = "Hello, world!";
expect!(greeting).to_start_with("Hello"); // Passes
expect!(greeting).not().to_start_with("Goodbye"); // Passes
}
to_end_with
Checks if a string ends with a specified suffix.
fn test_string_ends_with() {
let greeting = "Hello, world!";
expect!(greeting).to_end_with("world!"); // Passes
expect!(greeting).not().to_end_with("universe"); // Passes
}
to_match_regex
Checks if a string matches a regular expression pattern.
fn test_string_matches_regex() {
let greeting = "Hello, world!";
expect!(greeting).to_match_regex(r"^Hello.*!$"); // Passes
expect!(greeting).not().to_match_regex(r"^Goodbye.*\?$"); // Passes
}
to_have_length
Checks if a string has a specific length.
fn test_string_length() {
let greeting = "Hello, world!";
expect!(greeting).to_have_length(13); // Passes
expect!(greeting).not().to_have_length(10); // Passes
}
to_have_length_greater_than
Checks if a string length is greater than a specified value.
fn test_string_length_greater_than() {
let greeting = "Hello, world!";
expect!(greeting).to_have_length_greater_than(10); // Passes
expect!(greeting).not().to_have_length_greater_than(15); // Passes
}
to_have_length_less_than
Checks if a string length is less than a specified value.
fn test_string_length_less_than() {
let greeting = "Hello, world!";
expect!(greeting).to_have_length_less_than(20); // Passes
expect!(greeting).not().to_have_length_less_than(10); // Passes
}
Collection Matchers
to_be_empty_collection
Checks if a collection is empty.
fn test_empty_collection() {
let empty_vec: Vec<i32> = vec![];
let non_empty_vec = vec![1, 2, 3];
expect!(empty_vec.as_slice()).to_be_empty(); // Passes
expect!(non_empty_vec.as_slice()).not().to_be_empty(); // Passes
}
to_have_length_collection
Checks if a collection has a specific length.
fn test_collection_length() {
let numbers = vec![1, 2, 3, 4, 5];
expect!(numbers.as_slice()).to_have_length(5); // Passes
expect!(numbers.as_slice()).not().to_have_length(3); // Passes
}
to_contain_collection
Checks if a collection contains a specific element.
fn test_collection_contains() {
let numbers = vec![1, 2, 3, 4, 5];
expect!(numbers.as_slice()).to_contain(3); // Passes
expect!(numbers.as_slice()).not().to_contain(10); // Passes
}
to_contain_all_of
Checks if a collection contains all of the specified elements.
fn test_collection_contains_all() {
let numbers = vec![1, 2, 3, 4, 5];
expect!(numbers.as_slice()).to_contain_all_of(&[1, 3, 5]); // Passes
expect!(numbers.as_slice()).not().to_contain_all_of(&[1, 6, 7]); // Passes
}
to_equal_collection
Compares two collections for element-wise equality.
fn test_equal_collection() {
let numbers = vec![1, 2, 3, 4, 5];
expect!(numbers.as_slice()).to_equal_collection(vec![1, 2, 3, 4, 5]); // Passes
expect!(numbers.as_slice()).not().to_equal_collection(vec![5, 4, 3, 2, 1]); // Passes
}
HashMap Matchers
to_be_empty_hashmap
Checks if a HashMap is empty.
fn test_empty_hashmap() {
use std::collections::HashMap;
let empty_map: HashMap<&str, i32> = HashMap::new();
let mut non_empty_map = HashMap::new();
non_empty_map.insert("Alice", 100);
expect!(&empty_map).to_be_empty(); // Passes
expect!(&non_empty_map).not().to_be_empty(); // Passes
}
to_have_length_hashmap
Checks if a HashMap has a specific number of entries.
fn test_hashmap_length() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
expect!(&scores).to_have_length(2); // Passes
expect!(&scores).not().to_have_length(1); // Passes
}
to_contain_key
Checks if a HashMap contains a specific key.
fn test_hashmap_contains_key() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
expect!(&scores).to_contain_key("Alice"); // Passes
expect!(&scores).not().to_contain_key("Charlie"); // Passes
}
to_contain_entry
Checks if a HashMap contains a specific key-value pair.
fn test_hashmap_contains_entry() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
expect!(&scores).to_contain_entry("Alice", &100); // Passes
expect!(&scores).not().to_contain_entry("Alice", &50); // Passes
}
Option Matchers
to_be_some
Checks if an Option contains a value.
fn test_option_some() {
let maybe_value: Option<i32> = Some(42);
let empty_option: Option<i32> = None;
expect!(&maybe_value).to_be_some(); // Passes
expect!(&empty_option).not().to_be_some(); // Passes
}
to_be_none
Checks if an Option is None.
fn test_option_none() {
let maybe_value: Option<i32> = Some(42);
let empty_option: Option<i32> = None;
expect!(&empty_option).to_be_none(); // Passes
expect!(&maybe_value).not().to_be_none(); // Passes
}
to_contain_value
Checks if an Option contains a specific value.
fn test_option_contains_value() {
let maybe_value: Option<i32> = Some(42);
let other_value: Option<i32> = Some(100);
expect!(&maybe_value).to_contain_value(42); // Passes
expect!(&other_value).not().to_contain_value(42); // Passes
}
Result Matchers
to_be_ok
Checks if a Result is Ok.
fn test_result_ok() {
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("failed");
expect!(&success).to_be_ok(); // Passes
expect!(&failure).not().to_be_ok(); // Passes
}
to_be_err
Checks if a Result is Err.
fn test_result_err() {
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("failed");
expect!(&failure).to_be_err(); // Passes
expect!(&success).not().to_be_err(); // Passes
}
to_contain_ok
Checks if a Result contains a specific Ok value.
fn test_result_contains_ok() {
let success: Result<i32, &str> = Ok(42);
let other_success: Result<i32, &str> = Ok(100);
expect!(&success).to_contain_ok(42); // Passes
expect!(&other_success).not().to_contain_ok(42); // Passes
}
to_contain_err
Checks if a Result contains a specific error value.
fn test_result_contains_err() {
let network_err: Result<i32, &str> = Err("network error");
let auth_err: Result<i32, &str> = Err("authentication error");
expect!(&network_err).to_contain_err("network error"); // Passes
expect!(&auth_err).not().to_contain_err("network error"); // Passes
}
Custom Matchers
You can easily extend FluentTest with your own matchers:
// Define a custom matcher for your domain
trait UserMatchers<T> {
fn to_be_admin(self);
}
// Implement it for the Expectation type
impl<T: AsRef<User>> UserMatchers<T> for Expectation<T> {
fn to_be_admin(self) {
let user = self.value.as_ref();
let success = user.role == Role::Admin;
let not = if self.negated { " not" } else { "" };
if (success && !self.negated) || (!success && self.negated) {
self.report_success(&format!("is{not} an admin"));
} else {
let expected_msg = format!("Expected {}{not} to be an admin", self.expr_str);
self.report_failure(&expected_msg, &format!("Found role: {:?}", user.role));
}
}
}
// Use it in your tests
#[test]
fn test_user_permissions() {
let admin_user = get_admin_user();
let regular_user = get_regular_user();
expect!(admin_user).to_be_admin(); // Passes
expect!(regular_user).not().to_be_admin(); // Passes
}
Output Customization
FluentTest enhances the standard test output with colors, symbols, and improved formatting.
For CI environments or other special cases, you can customize the output:
// In your test module or test helper file
#[test]
fn setup() {
fluent_test::config()
.use_colors(true)
.use_unicode_symbols(true)
.show_success_details(false)
.apply();
}
How It Works
FluentTest is built around a few core components:
- The
expect!
macro which captures both values and their textual representation - The
expect_not!
macro which creates negated expectations - The
Expectation<T>
struct which holds the value and provides the fluent API - Trait implementations for different types of assertions
- A custom test reporter that enhances the standard output
Releases
This project is automatically published to crates.io when:
- The version in Cargo.toml is increased beyond the latest git tag
- The code is merged to the master branch
- All CI checks pass (tests, examples, linting)
The publishing workflow will:
- Create a git tag for the new version (vX.Y.Z)
- Publish the package to crates.io
- Generate a GitHub release using notes from CHANGELOG.md
- Fall back to auto-generated notes if no CHANGELOG entry exists
License
This project is licensed under the MIT License - see the LICENSE file for details.
Dependencies
~2.2–9.5MB
~87K SLoC