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

Download history

55 downloads per month

MIT license

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

Equality

  • to_equal - Checks if a value equals another value

Numeric

String

Collection

HashMap

Option

Result

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:

  1. The expect! macro which captures both values and their textual representation
  2. The expect_not! macro which creates negated expectations
  3. The Expectation<T> struct which holds the value and provides the fluent API
  4. Trait implementations for different types of assertions
  5. A custom test reporter that enhances the standard output

Releases

This project is automatically published to crates.io when:

  1. The version in Cargo.toml is increased beyond the latest git tag
  2. The code is merged to the master branch
  3. All CI checks pass (tests, examples, linting)

The publishing workflow will:

  1. Create a git tag for the new version (vX.Y.Z)
  2. Publish the package to crates.io
  3. Generate a GitHub release using notes from CHANGELOG.md
  4. 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