Expand description
§assert-struct: Ergonomic Structural Assertions
assert-struct is a procedural macro that enables clean, readable assertions for complex
data structures without verbose field-by-field comparisons. When assertions fail, it provides
clear, actionable error messages showing exactly what went wrong, including field paths and
expected vs actual values.
This comprehensive guide teaches you how to use assert-struct effectively in your tests.
After reading this documentation, you’ll be familiar with all capabilities and able to
leverage the full power of structural assertions.
§Table of Contents
§Quick Start
Add to your Cargo.toml:
[dev-dependencies]
assert-struct = "0.2"Basic example:
use assert_struct::assert_struct;
#[derive(Debug)]
struct User {
name: String,
age: u32,
email: String,
}
let user = User {
name: "Alice".to_string(),
age: 30,
email: "alice@example.com".to_string(),
};
// Only check the fields you care about
assert_struct!(user, User {
name: "Alice",
age: 30,
.. // Ignore email
});§Core Concepts
§Basic Assertions
The simplest use case is asserting all fields of a struct:
let point = Point { x: 10, y: 20 };
assert_struct!(point, Point {
x: 10,
y: 20,
});String fields work naturally with string literals:
let msg = Message {
text: "Hello world".to_string(),
urgent: false,
};
assert_struct!(msg, Message {
text: "Hello world", // No .to_string() needed!
urgent: false,
});§Partial Matching
Use .. to ignore fields you don’t want to check:
// Only verify name and email, ignore id and created_at
assert_struct!(user, User {
name: "Alice",
email: "alice@example.com",
..
});§Nested Structures
Assert on deeply nested data without repetitive field access:
assert_struct!(order, Order {
customer: Customer {
name: "Bob",
address: Address {
city: "Paris",
country: "France",
},
},
total: 99.99,
});
// Or with partial matching
assert_struct!(order, Order {
customer: Customer {
name: "Bob",
address: Address { city: "Paris", .. },
..
},
..
});
// Direct nested field access (no need to nest structs)
assert_struct!(order, Order {
customer.name: "Bob",
customer.address.city: "Paris",
customer.address.country: "France",
total: > 50.0,
..
});§Pattern Types
§Comparison Operators
Use comparison operators for numeric assertions:
assert_struct!(metrics, Metrics {
cpu: < 80.0, // Less than 80%
memory: <= 2048, // At most 2GB
requests: > 100, // More than 100
});All comparison operators work: <, <=, >, >=
§Equality Operators
Use explicit equality for clarity:
assert_struct!(status, Status {
code: == 200, // Explicit equality
active: != false, // Not equal to false
});§Range Patterns
Use ranges for boundary checks:
assert_struct!(person, Person {
age: 18..=65, // Working age range
score: 0.0..100.0, // Valid score range
});§Regex Patterns
Match string patterns with regular expressions (requires regex feature, enabled by default):
assert_struct!(account, Account {
username: =~ r"^[a-z_]+$", // Lowercase and underscores
email: =~ r"@company\.com$", // Company email domain
});§Method Call Patterns
Call methods on fields and assert on their results:
assert_struct!(data, Data {
content.len(): 11, // String length
items.len(): >= 5, // Vector size check
metadata.is_some(): true, // Option state
cache.contains_key("key1"): true, // HashMap lookup
..
});Method calls work with arguments too:
assert_struct!(text, Text {
content.starts_with("hello"): true,
..
});
assert_struct!(text, Text {
content.contains("world"): true,
..
});§Data Types
§Collections (Vec/Slice)
Element-wise pattern matching for vectors:
// Exact matching
assert_struct!(data, Data {
values: [5, 15, 25],
names: ["alice", "bob"], // String literals work in slices too!
});
// Pattern matching for each element
assert_struct!(data, Data {
values: [> 0, < 20, >= 25], // Different pattern per element
names: ["alice", "bob"],
});Partial slice matching:
assert_struct!(data, Data {
items: [1, 2, ..], // First two elements, ignore rest
});
assert_struct!(data, Data {
items: [.., 4, 5], // Last two elements
});
assert_struct!(data, Data {
items: [1, .., 5], // First and last elements
});§Set Patterns
Use #(...) to assert that a collection contains elements matching the given patterns,
in any order. Each pattern must match a distinct element; unlike slice patterns [...],
position does not matter.
// Exact match - every element must be matched, in any order
assert_struct!(data, Data {
items: #(1, 2, 3),
tags: #("rust", "async"), // String literals work too
});
// Partial match - use .. to allow extra unmatched elements
assert_struct!(data, Data {
items: #(> 0, ..), // At least one element matching > 0
tags: #("rust", ..), // Contains "rust", possibly more
});Struct patterns work inside #(...) too, which is useful when asserting that certain
items are present in a collection regardless of their order:
// Assert that click and hover events are present, ignoring order and other events
assert_struct!(events, #(
_ { kind: "click", value: > 0 },
_ { kind: "hover" },
..
));Other pattern types also work inside #(...):
assert_struct!(report, Report {
scores: #(> 90, < 50, 18..=100), // Comparisons and ranges
results: #(None, Some(> 0), ..), // Enum patterns
});Empty and wildcard set patterns:
assert_struct!(data, Data {
empty: #(), // Exactly empty collection
any: #(..), // Any collection (wildcard - any contents or length)
});§Maps (HashMap/BTreeMap)
Pattern matching for map-like structures using duck typing (works with any type that has len() and get() methods):
// Exact matching (checks map length)
assert_struct!(config, Config {
settings: #{ "theme": "dark", "language": "en" },
flags: #{ "debug": true, "verbose": false },
});
// Partial matching (ignores map length)
assert_struct!(config, Config {
settings: #{ "theme": "dark", .. }, // Only check theme
flags: #{ "debug": true, .. }, // Only check debug flag
});Advanced map patterns with comparisons and nested patterns:
// Pattern matching with comparison operators
assert_struct!(analytics, Analytics {
metrics: #{
"views": > 500, // More than 500 views
"clicks": >= 10, // At least 10 clicks
"conversions": > 0, // Some conversions
..
},
metadata: #{
"version": =~ r"^v\d+\.\d+\.\d+$", // Semantic version pattern
"environment": != "development", // Not in development
..
},
});Empty and wildcard map matching:
assert_struct!(data, Data {
cache: #{}, // Exactly empty map (len() == 0)
config: #{ .. }, // Any map content (wildcard)
});§Tuples
Full support for multi-field tuples:
// Basic tuple matching
assert_struct!(data, Data {
point: (15, 25),
metadata: ("info", 100, true), // String literals work in tuples!
});
// Advanced patterns
assert_struct!(data, Data {
point: (> 10, < 30), // Comparison operators
metadata: ("info", >= 50, true),
});Tuple method calls:
assert_struct!(data, Data {
coords: (0.len(): 8, 1.len(): 3), // Method calls on tuple elements
});§Enums (Option/Result/Custom)
§Option Types
assert_struct!(user, User {
name: Some("Alice"),
age: Some(30),
});
// Advanced patterns inside Option
assert_struct!(user, User {
name: Some("Alice"),
age: Some(>= 18), // Adult check inside Some
});§Result Types
assert_struct!(response, Response {
result: Ok("success"),
});
// Pattern matching inside Result
assert_struct!(response, Response {
result: Ok(=~ r"^user\d+$"), // Regex inside Ok
});§Custom Enums
// Unit variants
let active_account = Account { status: Status::Active };
assert_struct!(active_account, Account {
status: Status::Active,
});
// Struct variants with partial matching
assert_struct!(account, Account {
status: Status::Pending { since: "2024-01-01" },
});§Smart Pointers
Dereference smart pointers directly in patterns:
assert_struct!(cache, Cache {
*data: "cached", // Dereference Arc<String>
*count: > 40, // Dereference Box<i32> with comparison
*shared: true, // Dereference Rc<bool>
});Multiple dereferencing for nested pointers:
assert_struct!(nested, Nested {
**value: 42, // Double dereference
});§Wildcard Patterns
Use wildcard patterns (_) to avoid importing types while still asserting on their structure:
// No need to import User or Metadata types!
// Wildcard patterns always do partial matching, so .. is never required.
assert_struct!(response, _ {
user: _ {
id: 123,
name: "Alice",
},
metadata: _ {
version: "1.0",
},
});This is particularly useful when testing API responses where you don’t want to import all the nested types:
// Test deeply nested structures without imports
assert_struct!(json_response, _ {
data: _ {
items: [_ { id: 1, value: "test" }],
total: 1,
},
});§Error Messages
When assertions fail, assert-struct provides detailed, actionable error messages:
§Basic Mismatch
assert_struct!(user, User {
name: "Bob", // This will fail
age: 25,
});
// Error output:
// assert_struct! failed:
//
// value mismatch:
// --> `user.name` (src/lib.rs:456)
// actual: "Alice"
// expected: "Bob"§Comparison Failure
assert_struct!(stats, Stats {
score: > 100, // This will fail
});
// Error output:
// assert_struct! failed:
//
// comparison mismatch:
// --> `stats.score` (src/lib.rs:469)
// actual: 50
// expected: > 100§Nested Field Errors
Error messages show the exact path to the failing field, even in deeply nested structures. Method calls are also shown in the field path for clear debugging.
§Advanced Usage
§Mixing Structural and Operator Patterns
Structural patterns and operator patterns are complementary — within a single
assert_struct! call, use whichever is most concise for each field. The goal is
the shortest assertion that expresses what you actually care about.
Structural patterns (SomeType { field: value, .. }) are great for navigating
nested types. But for a specific field, writing out the full structural equivalent
can be far more verbose than a simple == or =~ check — especially when the
field’s type implements PartialEq or PartialOrd with a simpler type.
For example, consider a Value enum (similar to serde_json::Value) that can
hold different primitive types and implements PartialEq with them:
// Structural matching navigates the outer types; operator pattern handles Value
assert_struct!(query, Query {
table: "users",
filter: Some(_ {
column: 2,
value: == "alice", // much shorter than Value::Text("alice".to_string())
}),
});The == "alice" works because Value: PartialEq<&str>. The same applies to
comparison operators — if Value: PartialOrd<i64>, you can write value: > 0i64
instead of matching the variant.
The general rule: if writing the structural pattern for a field requires naming internal types or constructing wrapper values, check whether an operator pattern expresses the same intent more directly.
§Pattern Composition
Combine multiple patterns for comprehensive assertions:
assert_struct!(complex, Complex {
data: Some([> 0, > 1, > 2]), // Option + Vec + comparisons
metadata: ("info", > 40), // Tuple + string + comparison
..
});
// Verify data length separately
assert_eq!(complex.data.as_ref().unwrap().len(), 3);§Real-World Testing Patterns
See the examples directory for comprehensive real-world examples including:
- API response validation
- Database record testing
- Configuration validation
- Event system testing
For complete specification details, see the assert_struct! macro documentation.
Macros§
- assert_
struct - Structural assertion macro for testing complex data structures.
Traits§
- Like
- A trait for pattern matching, similar to
PartialEqbut for flexible matching.