Integration Testing
Property-Based Testing - External Tooling
While traditional unit tests verify specific hand-picked input examples (assert_eq!(reverse(&[1, 2]), vec![2, 1])), property-based testing verifies universal invariants ("properties") across hundreds or thousands of randomly generated inputs, automatically shrinking any failing input down to the smallest minimal counterexample.
Property-Based Testing Workflow (`proptest` / `quickcheck`):
[ Strategy / Generator ] ---> Generates 256+ random inputs (e.g., Vec<i32>, String)
|
v
[ Property Function ]
e.g., decode(encode(x)) == x
|
+----------------------+----------------------+
| |
All 256 pass Failure found!
--> Test "ok" e.g., [-94812, 0, 4192]
|
v
[ Automatic Shrinking ]
Simplifies to minimal
failing input: [0, -1]
Categories of Universal Properties
| Property Pattern | Invariant Formula | Example |
|---|---|---|
| Round-Trip (Invertibility) | decode(encode(x)) == Ok(x) |
Serialization (serde), compression, base64, URL encoding. |
| Idempotence | f(f(x)) == f(x) |
Normalization, sorting, deduplication, formatting (rustfmt). |
| Structural Invariant | postcondition(f(x)) == true |
sort(v).len() == v.len() and is_sorted(&sort(v)). |
| Test Oracle (Differential) | fast_optimized(x) == simple_reference(x) |
Comparing a SIMD or concurrent algorithm against a naive 5-line reference implementation. |
| Commutativity / Associativity | merge(a, b) == merge(b, a) |
CRDT state merging, set operations, numeric aggregations. |
Ecosystem Crates: proptest vs quickcheck
Rust's ecosystem provides two widely adopted property-based testing crates added under [dev-dependencies]:
proptest: Uses explicit composable strategies (such as"[a-z]{1,10}"regex generators or range strategies0..1000i32), supports stateful shrinking across composed types, and automatically persists failing seeds to aproptest-regressions/file so discovered bugs are re-tested on every futurecargo testrun.quickcheck: Generates inputs based on type via theArbitrarytrait (fn prop(xs: Vec<i32>) -> bool), requiring minimal setup boilerplate.
[dev-dependencies]
proptest = "1"
// Example of a `proptest!` macro invocation when using the `proptest` crate:
//
// proptest::proptest! {
// #[test]
// fn run_length_codec_round_trips(data in proptest::collection::vec(0u8..=5, 0..200)) {
// let encoded = rle_encode(&data);
// let decoded = rle_decode(&encoded);
// proptest::prop_assert_eq!(decoded, data);
// }
// }
Demonstrating Property Checking and Input Shrinking
The following self-contained example demonstrates how a property runner generates inputs from a deterministic PRNG seed, checks a round-trip invariant, and shrinks a failing input to its minimal counterexample:
pub fn rle_encode(input: &[u8]) -> Vec<(u8, u8)> {
let mut out = Vec::new();
for &byte in input {
match out.last_mut() {
// Bug if a run exceeds 255, or here intentionally capped at 3 for demonstration:
Some((val, count)) if *val == byte && *count < 255 => *count += 1,
_ => out.push((byte, 1)),
}
}
out
}
pub fn rle_decode(encoded: &[(u8, u8)]) -> Vec<u8> {
let mut out = Vec::new();
for &(byte, count) in encoded {
out.extend(std::iter::repeat_n(byte, count as usize));
}
out
}
// Simple deterministic xorshift64 generator for self-contained property verification
struct XorShift64(u64);
impl XorShift64 {
fn next_u64(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
}
fn main() {
let mut rng = XorShift64(0xDEAD_BEEF_CAFE_BABE);
for _case in 0..500 {
let len = (rng.next_u64() % 128) as usize;
let data: Vec<u8> = (0..len).map(|_| (rng.next_u64() % 4) as u8).collect();
// Property 1: Round-trip invertibility
let encoded = rle_encode(&data);
let decoded = rle_decode(&encoded);
assert_eq!(decoded, data, "RLE round-trip failed for input {data:?}");
// Property 2: Decoded length equals sum of run counts
let sum_counts: usize = encoded.iter().map(|&(_, c)| c as usize).sum();
assert_eq!(sum_counts, data.len());
}
println!("Verified RLE round-trip and length properties across 500 generated inputs");
}
Verified RLE round-trip and length properties across 500 generated inputs