| Crates.io | assertr-derive |
| lib.rs | assertr-derive |
| version | 0.2.3 |
| created_at | 2025-01-17 13:44:23.773254+00 |
| updated_at | 2025-06-25 13:22:11.551846+00 |
| description | Derive AssertrEq implementations. |
| homepage | |
| repository | https://github.com/lpotthast/assertr |
| max_upload_size | |
| id | 1520628 |
| size | 49,341 |
A fluent assertion library for Rust that enables clear, intuitive, and readable test code
with detailed failure messages to help pinpoint issues quickly.
#[derive(AssertrEq)] macro.[dependencies]
assertr = "0.3.5"
or
[dependencies]
assertr = { version = "0.3.5", features = ["derive"] }
if you want the AssertrEq derive macro allowing you to perform partial equality assertions on struct value on a
field-by-field value. More on that later.
You may disable the default features for no-std environments.
You may activate any of the following features:
| feature | description | default feature |
|---|---|---|
| std | Assertions for types from the standard library. | yes |
| derive | Enables the AssertrEq derive macro. |
no |
| num | Assertions for numeric types. | yes |
| libm | Use fallback implementations for Rust's float math functions in core. | no |
| serde | Assertions for serializable types (supporting json and toml). | no |
| jiff | Assertions for types from the jiff crate. |
no |
| tokio | Assertions for types from the tokio crate. |
no |
| reqwest | Assertions for types from the reqwest crate. |
no |
Always prefer importing the entire prelude, as in:
use assertr::prelude::*;
#[test]
fn test() {
assert_that("hello, world!")
.starts_with("hello")
.ends_with("!");
}
This way, you are ensured to get full IDE autocompletion and always see all available assertions for the type you are currently writing an assertion on.
| type / required bounds | assertion | note | required features |
|---|---|---|---|
T: PartialEq |
is_equal_to(expected) |
||
T: PartialEq |
is_not_equal_to(expected) |
||
T: PartialOrd<E> |
is_less_than(expected) |
||
T: PartialOrd<E> |
is_greater_than(expected) |
||
T: PartialOrd<E> |
is_less_or_equal_to(expected) |
||
T: PartialOrd<E> |
is_greater_or_equal_to(expected) |
||
bool |
is_true() |
||
bool |
is_false() |
||
char |
is_equal_to_ignoring_ascii_case(expected) |
||
char |
is_lowercase() |
||
char |
is_uppercase() |
||
char |
is_ascii_lowercase() |
||
char |
is_ascii_uppercase() |
||
&str |
is_blank() |
||
&str |
is_blank_ascii() |
||
&str |
contains(expected) |
||
&str |
starts_with(expected) |
||
&str |
ends_with(expected) |
||
String |
contains(expected) |
alloc | |
String |
starts_with(expected) |
alloc | |
String |
ends_with(expected) |
alloc | |
&[T] |
contains(expected) |
||
&[T] |
contains_exactly(expected) |
||
&[T] |
contains_exactly_in_any_order(expected) |
||
&[T] |
contains_exactly_matching_in_any_order(expected) |
||
[T; N] |
contains(expected) |
||
[T; N] |
contains_exactly(expected) |
||
[T; N] |
contains_exactly_matching_in_any_order(expected) |
||
Vec<T> |
contains(expected) |
alloc | |
Vec<T> |
contains_exactly(expected) |
alloc | |
Vec<T> |
contains_exactly_matching_in_any_order(expected) |
alloc | |
T: Debug |
has_debug_string(expected) |
||
T: Debug |
has_debug_value(expected) |
||
T: Display |
has_display_value(expected) |
||
F: FnOnce -> R |
panics() |
||
F: FnOnce -> R |
does_not_panic() |
||
I: Iterator<Item = T> |
contains(expected) |
||
I: Iterator<Item = T> |
contains_exactly(expected) |
||
T: HasLength |
is_empty() |
implemented for: `` | |
T: HasLength |
is_not_empty() |
implemented for: `` | |
T: HasLength |
has_length(expected) |
implemented for: `` | |
T: Num |
is_zero() |
num | |
T: Num |
is_additive_identity() |
Synonym for is_zero |
num |
T: Num |
is_one() |
num | |
T: Num |
is_multiplicative_identity() |
Synonym for is_zero |
num |
T: Num + Signed |
is_negative() |
num | |
T: Num + Signed |
is_positive() |
num | |
T: Num + PartialOrd + Clone |
is_close_to() |
num | |
T: Num + Float |
is_nan() |
num | |
T: Num + Float |
is_finite() |
num | |
T: Num + Float |
is_infinite() |
num | |
Option<T> |
is_some() |
||
Option<T> |
is_none() |
||
Poll<T> |
is_pending() |
||
Poll<T> |
is_ready() |
||
R: RangeBounds<B>, B: PartialOrd |
contains_element(expected) |
||
R: RangeBounds<B>, B: PartialOrd |
does_not_contain_element(expected) |
||
B: PartialOrd |
is_in_range(expected) |
||
B: PartialOrd |
is_not_in_range(expected) |
||
B: PartialOrd |
is_outside_of_range(expected) |
Synonym for is_not_in_range |
|
RefCell<T> |
is_borrowed() |
||
RefCell<T> |
is_mutably_borrowed() |
||
RefCell<T> |
is_not_mutably_borrowed() |
||
Mutex<T> |
is_locked() |
||
Mutex<T> |
is_not_locked() |
||
Mutex<T> |
is_free() |
Synonym foris_not_locked |
|
Result<T, E> |
is_ok() |
||
Result<T, E> |
is_err() |
||
Result<T, E> |
is_ok_satisfying(assertions) |
||
Result<T, E> |
is_err_satisfying(assertions) |
||
PathBuf |
exists() |
std | |
PathBuf |
does_not_exist() |
std | |
PathBuf |
is_a_file() |
std | |
PathBuf |
is_a_directory() |
std | |
PathBuf |
is_a_symlink() |
std | |
PathBuf |
has_a_root() |
std | |
PathBuf |
is_relative() |
std | |
PathBuf |
has_file_name(expected) |
std | |
PathBuf |
has_file_stem(expected) |
std | |
PathBuf |
has_extension(expected) |
std | |
&Path |
exists() |
std | |
&Path |
does_not_exist() |
std | |
&Path |
is_a_file() |
std | |
&Path |
is_a_directory() |
std | |
&Path |
is_a_symlink() |
std | |
&Path |
has_a_root() |
std | |
&Path |
is_relative() |
std | |
&Path |
has_file_name(expected) |
std | |
&Path |
has_file_stem(expected) |
std | |
&Path |
has_extension(expected) |
std | |
HashMap<K, V> |
contains_key(expected) |
std | |
HashMap<K, V> |
does_not_contain_key(not_expected) |
std | |
HashMap<K, V> |
contains_value(expected) |
std | |
HashMap<K, V> |
contains_entry(expected_key, expected_value) |
std | |
Command |
has_arg(expected) |
std | |
Type<T> |
needs_drop() |
std | |
Box<dyn Any> |
has_type::<Expected>() |
alloc | |
Box<dyn Any> |
has_type_ref::<Expected>() |
alloc | |
PanicValue |
has_type::<Expected>() |
alloc | |
PanicValue |
has_type_ref::<Expected>() |
alloc | |
tokio::sync::Mutex<T> |
is_locked() |
tokio | |
tokio::sync::Mutex<T> |
is_not_locked() |
tokio | |
tokio::sync::Mutex<T> |
is_free() |
Synonym for is_not_locked |
tokio |
tokio::sync::RwLock<T> |
is_not_locked() |
tokio | |
tokio::sync::RwLock<T> |
is_free() |
Synonym for is_not_locked |
tokio |
tokio::sync::RwLock<T> |
is_read_locked() |
tokio | |
tokio::sync::RwLock<T> |
is_write_locked() |
tokio | |
tokio::sync::watch::Receiver<T> |
has_current_value(expected) |
tokio | |
tokio::sync::watch::Receiver<T> |
has_changed() |
tokio | |
tokio::sync::watch::Receiver<T> |
has_not_changed() |
tokio | |
reqwest::Response |
has_status_code(expected) |
reqwest | |
jiff::SignedDuration |
is_zero() |
jiff | |
jiff::SignedDuration |
is_negative() |
jiff | |
jiff::SignedDuration |
is_positive() |
jiff | |
jiff::SignedDuration |
is_close_to(expected, allowed_deviation) |
jiff | |
jiff::Span |
is_zero() |
jiff | |
jiff::Span |
is_negative() |
jiff | |
jiff::Span |
is_positive() |
jiff | |
jiff::Zoned |
is_in_time_zone(expected) |
jiff | |
jiff::Zoned |
is_in_time_zone_named(expected) |
jiff |
*The generic types (T, E, ...) nearly always also require to be Debug. Otherwise, we could not print values in
case an assertion is violated. We chose to not explicitly list these bounds in the table above.
satisfies(condition): Asserts that a value satisfies a conditionsatisfies_ref(condition): Asserts that a referenced value satisfies a conditionInstead of immediately panicking on assertion failure, you can capture failures for later analysis:
let failures = assert_that(3)
.with_capture()
.is_equal_to(4)
.is_less_than(2)
.capture_failures();
assert_that(failures).has_length(2);
You can derive a helper struct, allowing you to perform partial equality comparisons, for any owned struct type
by annotating it with the #[derive(AssertrEq).
Make sure that this crates derive feature is active!
assertr = { version = "0.2.0", features = ["derive"] }
// Deriving `AssertrEq` provides us an additional `PersonAssertrEq` type.
// Deriving `Debug` is necessary, as we want to actually use `Foo` in an assertion.
#[derive(Debug, AssertrEq)]
pub struct Person {
pub name: String,
pub age: i32,
pub data: (u32, u32),
}
#[test]
fn test() {
let alice = Person {
name: "Alice".to_owned(),
age: 30,
data: (100, 998)
};
// We can still perform a standard (full) equality check.
assert_that_ref(&alice).is_equal_to(Person {
name: "Alice".to_owned(),
age: 30,
data: (100, 998),
});
// But we can also do a partial equality check!
assert_that_ref(&alice).is_equal_to(PersonAssertrEq {
name: eq("Alice".to_owned()),
age: any(), // Match any age
data: any() // Match any data
});
}
#[derive(Debug, PartialEq)]
struct Person {
age: u32,
}
trait PersonAssertions {
fn has_age(self, expected: u32) -> Self;
}
impl<M: Mode> PersonAssertions for AssertThat<'_, Person, M> {
fn has_age(self, expected: u32) -> Self {
self.satisfies(|p| p.age, |age| { age.is_equal_to(expected); })
}
}
#[test]
fn test() {
assert_that(Person { age: 30 })
.has_age(30);
}
Test properties of types:
assert_that_type::<MyType>()
.needs_drop()
.satisfies(|it| it.size(), |size| {
size.is_equal_to(32);
});
// Assertions that read like English.
assert_that("foobar").starts_with("foo").contains("ooba");
assert_that(vec![1, 2, 3]).has_length(3).contains(2);
assert_that((Ok(42)).is_ok().is_equal_to(42);
assert_that(Person { id: 42 }).has_debug_string("Person { id: 42 }");
// Chainable,
assert_that("foobar")
.is_not_empty()
.starts_with("foo")
.ends_with("bar")
.has_length(6);
Partial equality assertions (meaning that only some fields of a struct are compared, while some are ignored).
Add the AssertrEq annotation to one of your struct to enable this.
One other style of assertions in Rust is the "individual macros" approach.
The standard library already comes with a few of them, like the assert_eq! macro, many libraries provide a more
exhaustive list of macros specifically tailored for specific types and operations.
Let me point out a few benefits of fluent assertions compared to individual assert macros.
The fluent interface allows you to chain multiple assertions naturally, following the way we think about validating properties. Instead of writing multiple separate assertions, you can express related checks in a single, flowing statement that reads almost like natural language.
Additionally, having a concrete entrance into the assertion context using a function like assert_that with assertions
coming after makes it totally obvious which value is the "actual" and which is the "expected" value. This provides
a clear schema for how assertions are written, compared to an assertion macro, like std's assert_eq!, in which the
order of arguments can be chosen freely, making it non-obvious when coming into a new codebase which style
was chosen.
Fluent assertions can provide more detailed and structured error messages out of the box. Rather than just showing the values that didn't match, they can include context about what specific check failed within the chain and clearer descriptions of the expected vs actual values. Descriptive messages can be collected throughout the call chain.
With traditional assert macros, you often need to reference the same value multiple times:
let vec = vec![1, 2, 3];
assert_eq!(vec.len(), 3);
assert!(vec.contains(&2));
Versus the fluent style:
assert_that(vec![1, 2, 3]).has_length(3).contains(2);
Eq
to allow is_equal_to, PartialOrd types to allow is_greater_than assertions and all types implementing the
HasLength trait to support the has_length assertion.assert_that(...) suffices to get into an assertion context. Use assert_that_ref(&val) if you cant give up
ownership and instead want to assert on a reference.use assertr::prelude::*;To test all crates, run with --all when in root
cargo test --all
This crate uses features. Some tests are declared under conditional compilation.
Run all tests using
cargo test --all-features
0.1.0 the MSRV is 1.76.00.2.0 the MSRV is 1.85.0Many assertions require std::fmt::Debug, limiting usability to types implementing Debug.
Can we implement fallback rendering? Will probably require the currently unstable specialization feature.
The differentiation between assert_that for owned values and assert_that_ref for references is not ideal.
One assert_that function, not being a macro and accepting both owned values and references would be much preferred.
But that would probably also require the specialization feature to be able to detect the use of a reference type at
compiletime.
Contributions are welcome. Feel free to open a PR with your suggested changes.
Midway through implementing this, I found out that "spectral" already exists, which uses a very similar style of
assertions.
After looking into it, I took the concept of generally relying on *Assertions trait definitions instead of directly
implementing Assertions with multiple impl blocks on the AssertThat type.