| name | property-based-testing |
| description | Expert guidance on adding property-based tests to mobile codebases using kotest-property, SwiftCheck, glados (Dart), and fast-check (TS). Use when example-based tests feel incomplete or when testing parsers, serializers, and state machines. |
Property-Based Testing on Mobile
Instructions
Property-based testing (PBT) generates many inputs and asserts invariants that must hold for all of them. It is most valuable for parsers, serializers, mappers, reducers, pricing rules, and state machines — places where example-based tests only cover a handful of rows but the input space is large.
1. Properties, Not Examples
An example asserts f(3) == 9. A property asserts "for all non-negative n, sqrt(n*n) == n". Write down the invariant first, then encode it.
Common invariant shapes:
- Round-trip:
decode(encode(x)) == x.
- Idempotence:
f(f(x)) == f(x) (e.g., normalization).
- Commutativity / associativity: where the domain admits it.
- Monotonicity: adding an item never decreases cart total.
- Never-throws:
parse(s) returns Result for any String.
2. Kotlin — kotest-property
class MoneyProps : StringSpec({
"addition is commutative" {
checkAll(Arb.money(), Arb.money()) { a, b ->
(a + b) shouldBe (b + a)
}
}
"rounding is idempotent" {
checkAll(Arb.bigDecimal()) { x ->
Money.round(Money.round(x)) shouldBe Money.round(x)
}
}
})
Custom generator:
fun Arb.Companion.money() = Arb.bigDecimal(
min = BigDecimal("-1000000"), max = BigDecimal("1000000")
).map(Money::usd)
3. Swift — SwiftCheck (or swift-testing custom generators)
import SwiftCheck
property("reverse is an involution") <- forAll { (xs: [Int]) in
xs.reversed().reversed() == xs
}
On newer Swift Testing code, you can hand-roll with @Test(arguments: ...) over a seeded random generator to approximate PBT until a library-level option matures for your project.
4. Dart — glados
void main() {
Glados<int>().test('abs is non-negative', (n) {
expect(n.abs() >= 0, isTrue);
});
}
Combine generators:
Glados2<int, int>().test('min(a,b) <= max(a,b)', (a, b) {
expect(min(a, b) <= max(a, b), isTrue);
});
5. TypeScript / React Native — fast-check
import fc from 'fast-check';
test('JSON round-trips', () => {
fc.assert(fc.property(fc.jsonValue(), (v) => {
expect(JSON.parse(JSON.stringify(v))).toEqual(v);
}));
});
fast-check ships arbitraries for dates, unicode strings, records, and a shrink mechanism that returns a minimal failing case.
6. Shrinking
The payoff of PBT is minimal failing examples. Do not wrap the body in try { ... } catch { /* swallow */ } — you will lose the shrink. Let the assertion throw and the framework will shrink the counterexample for you.
7. Determinism
- Seed the generator. All four libraries accept an explicit seed; log it on failure and use it to reproduce.
- Cap sample count in CI (e.g., 100–200 runs) and boost locally or nightly (e.g., 5 000).
- Keep PBT out of the fast "every save" loop if it exceeds ~1 s per property — move long ones to a nightly job.
8. When NOT to Use PBT
- Tests that assert on one canonical example from a spec (RFC, product decision) — keep them explicit.
- Very wide input spaces with shallow invariants — you will chase shrink noise.
- UI rendering — snapshot tests fit better.
9. Integrating with Existing Suites
Run PBT alongside example-based tests in the same runner. Keep a small number of regression examples pinned as example-based tests whenever PBT finds a bug — the property catches the class, the example pins the specific one.
10. Checklist