Property-Based Testing with StreamData
Catch deep edge cases with property-based testing in Elixir using StreamData. Covers generators, shrinking, invariants, and integration with ExUnit.
Example tests are necessary but limited. Property-based testing checks that general rules hold across many randomized inputs.
Example vs Property
Example test:
test "sum adds two numbers" do
assert add(2, 3) == 5
end
Property test:
use ExUnitProperties
property "adding zero returns same value" do
check all x <- integer() do
assert add(x, 0) == x
end
end
The property validates an invariant for many values, not one.
Generator Design
Useful generators reflect realistic domains:
- bounded ranges,
- valid structured maps,
- combinations that include edge values.
def user_gen do
fixed_map(%{
age: integer(0..120),
email: string(:alphanumeric, min_length: 3),
active: boolean()
})
end
Shrinking and Debugging
When a property fails, StreamData shrinks toward a minimal failing input. This is often the fastest path to root cause.
Treat failing shrunk values as regression fixtures in regular tests.
High-Value Property Targets
- parsers/serializers round-trip behavior,
- sorting and set invariants,
- idempotency rules,
- normalization/canonicalization logic,
- permission checks under combinatorial inputs.
# Hypothesis
# Similar concept: generators + properties + shrinking.
// fast-check
// Similar concept in JS ecosystem.
# StreamData
# Native ExUnit integration for property checks and shrinking.
Exercise
Property-Test a Normalization Pipeline
Choose one normalization function in your app and add properties:
- Idempotency (
f(f(x)) == f(x)). - Shape invariant (required keys always present).
- Range invariant (numeric bounds preserved).
- Add at least one custom generator for realistic inputs.
- Convert a discovered counterexample into a fixed regression test.
FAQ and Troubleshooting
My property test fails intermittently. Why?
You may rely on time/random/external state. Make the function deterministic or control side effects for test runs.
How many runs are enough?
Start with defaults, then increase for critical properties and CI stability thresholds.
Should property tests replace example tests?
No. Use both: examples for intent/readability, properties for broad invariant coverage.
Related Lessons
Key Takeaways
- Property tests validate invariants across many generated inputs, not single examples
- Good generator design is the foundation of useful property tests
- Shrinking makes failures actionable by producing minimal counterexamples