---
title: 'Testing and Property-Based Testing'
source: 'https://academia.sh/en/courses/rust-advanced/testing-and-property-based-testing'
course: 'The Rust Ecosystem and Advanced Topics'
language: en
updated: '2026-09-15T04:58:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tests a property across inputs produced by a fixed-seed, deterministic generator and counts how many inputs it holds for'
    - "Verifies across two hundred inputs that applying reversal twice yields identity, showing the guarantee's class (handed to the writer)"
    - 'Explains, with a property that holds for only some inputs, that a passing property test does not mean correct for every input'
---

# Testing and Property-Based Testing

Counting whether a property holds across two hundred inputs produced by a fixed-seed, deterministic generator, and showing within what limit this count answers a question the compiler never asks.

The Dependencies and Feature Flags lesson measured a body the compiler never sees at all —
there, the guarantee never even started. But the previous lesson's closing question pointed
in a different direction: a body like `running_body` enters compilation, passes type
checking, has its names resolved correctly, and can still produce a **wrong** result. The
compiler checks that a body exists and that its type is consistent; it never checks that the
body is *correct* — correctness is a property of behavior, not of type, and type checking
knows nothing about behavior.

One way to close this gap is to test by running: run a body against specific inputs and
check whether the result carries an expected property. This is different from writing an
example test with a single fixed input — an example test only shows that something is
correct for that one input, and says nothing about any input that was not chosen. This
lesson does this not with a single fixed input but with a large number of inputs produced by
a fixed-seed generator — that is property-based testing's core idea: instead of a single
example, test the same property across many generated examples. That the inputs come from a
deterministic generator is not a side condition, it is one of the necessary requirements for
the observation this lesson establishes: if a generator relying on the system clock or the
operating system's source of randomness were used, the same test would run with different
inputs on every machine and every run, and the number of inputs it held for would change
from run to run — this lesson rules out that variability from the start. The test itself is
not without checking: what decides which property is tested, which range the inputs come
from, and how many inputs are enough is not the compiler, it is **the party who writes the
test**. The course's shared question — where does the compiler's guarantee end here — finds
its answer in this lesson: where type checking ends, behavior checking begins, but the party
who writes it also draws this checking's scope.

## Testing a Property by Running

A property-based test is built from three parts: a source that produces random inputs, a
condition to test on each of those inputs, and a loop that counts how many inputs the
condition holds for. Rust's standard library has no random-number generator, and this
curriculum forbids third-party crates, so the generator is hand-written. **PL5.** The only
thing that needs to be random is the *distribution* of inputs — the test itself must stay
deterministic, or the same run would give a different result on different machines. The
generator below is a linear congruential generator that starts from a fixed seed and
produces the same sequence on every call: given the same seed, the output sequence is
bit-for-bit identical on every run.

The generator's state is a single `u64` number, updated on every call by multiplying and
adding two fixed numbers; `wrapping_mul` and `wrapping_add` silently wrap any overflow in
this operation, so it never halts with a panic. `length` narrows this raw number to an upper
bound (deciding how many elements an array carries), and `element` produces an integer
between `-500` and `499`. `vector` combines the two: it first draws a length, then produces
that many elements. The constants used come from a known family of linear congruential
generators, and the generator's *good distribution* is not this lesson's subject — the only
property sought here is that it produces the same sequence on every run with the same seed;
this property gave bit-for-bit identical numbers across every run above.

```rust
// linear_generator.rs — fixed-seed, deterministic pseudo-random generator
pub struct LinearGenerator {
    state: u64,
}

impl LinearGenerator {
    pub fn new(seed: u64) -> Self {
        LinearGenerator { state: seed }
    }

    pub fn next(&mut self) -> u64 {
        self.state = self
            .state
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        self.state
    }

    pub fn length(&mut self, upper_bound: usize) -> usize {
        (self.next() % (upper_bound as u64 + 1)) as usize
    }

    pub fn element(&mut self) -> i32 {
        (self.next() % 1000) as i32 - 500
    }

    pub fn vector(&mut self, max_len: usize) -> Vec<i32> {
        let n = self.length(max_len);
        (0..n).map(|_| self.element()).collect()
    }
}
```

The property to be tested is a classic example: reversing an array twice should return the
original array. This property is built using `Vec`'s `reverse` method and is tested against
every generated input. Two hundred inputs are produced, each carrying at most twenty
elements; the count of inputs the property holds for is tallied.

```rust
// property_test.rs — does applying reversal twice give identity
mod linear_generator;
use linear_generator::LinearGenerator;

fn reverse_reverse_identity(v: &[i32]) -> bool {
    let mut copy = v.to_vec();
    copy.reverse();
    copy.reverse();
    copy == v
}

fn main() {
    let mut generator = LinearGenerator::new(42);
    let total_inputs = 200;
    let mut held = 0;

    for _ in 0..total_inputs {
        let input = generator.vector(20);
        if reverse_reverse_identity(&input) {
            held += 1;
        }
    }

    println!("inputs tested: {}", total_inputs);
    println!("inputs the property held for: {}", held);
}
```

```
inputs tested: 200
inputs the property held for: 200
```

When the generator starts with seed `42`, the first array it produces carries fourteen
elements, the second eleven, the third eighteen — lengths, just like elements, change
deterministically with the fixed seed. Only the total and held counts are printed in this
run, but each of the two hundred arrays the generator produces arrives this way, with
different lengths and different elements; the upper bound given to `vector` (`20`) states
that an array can carry at most twenty elements, and at least zero (an empty array). The
smallest length was left at zero because an empty array is also a valid input — applying
reversal twice should also give identity on an empty array, and the generator does produce
this edge case from time to time.

Applying reversal twice is a small example chosen to make explaining property-based testing
easier; the same approach can be applied to larger bodies. Converting a structure a parser
produced back to a string and parsing it again should give the same result as the original
structure; a sorting function's output should carry the same elements as the input and have
its neighboring elements in order. The property chosen in this lesson is one of the
simplest — an operation combined with its own inverse gives identity — but the way of
measuring (generator, property, counter) stays the same: whatever body is tested, closing
the place the compiler does not check by running always requires the same three parts.

All two hundred inputs confirmed the property. **PL6.** This count is deterministic
together with the generator: as long as the seed is `42`, each of the two hundred arrays
produced arrives in the same order, the same length, and with the same elements; no matter
how many times the run is repeated, the result does not change. This also clarifies what the
word "random" means in this lesson: the produced inputs are *varied* (not written out one by
one by hand), but the production process is *not random* — it produces the same sequence on
every run.

**Distinguishing this from a related idea:** the Production Go: Testing, Tooling and
Performance course's Coverage Reporting lesson measured **which lines** a test ran — the
coverage ratio showed how much of the source passed through the run. This lesson does not
count lines; it counts **how many inputs** a property holds for. A line running does not
mean it runs correctly — it only means it ran; what this lesson measures is the strength of
the guarantee that running gives about correctness.

The guarantee's class here is **handed to the writer**: the compiler checked
`reverse_reverse_identity`'s type and stopped there; it never asked whether the function
gives the correct result *for every input*. The party who wrote the test was the one who
asked and answered this question — the writer decided which property to test, which range
the inputs would come from (`-500` to `499`, at most twenty elements), and how many inputs
(two hundred) would be enough. The compiler took no part in any of these decisions; it only
checked that the written test code itself was type-consistent.

The **handed-to-the-writer** class carries the same word as the transfer measured in the
`unsafe` Rust lesson, but it sits in a different place: there, what was handed off was the
contract that a memory access stays within bounds; here, what is handed off is the claim
that a behavior is *correct*. What the two share is that the compiler stops at some point
and leaves the remaining responsibility to whoever wrote the source. One consequence of this
transfer: if the `reverse_reverse_identity` function itself had been written wrong — say,
the second `reverse` call had been forgotten — the compiler would not have noticed that
either, because a wrongly written test passes type checking exactly like a correctly written
one. A test's correctness, just like the correctness of the body it tests, stays the
writer's responsibility.

How many inputs there are is also part of the same transfer. Two hundred was chosen in this
lesson; had fifty been chosen, the test would have seen fewer inputs, had two thousand been
chosen, more. As the number grows, the covered set of inputs widens, but no number covers
**all** inputs. The compiler expresses no opinion about this number at all — no matter how
small or how large the number is, the test code passes type checking and compiles the same
way. Deciding whether the number is sufficient is part of the same responsibility as
deciding the generator's range: both rest in the writer's hands, entirely outside the
compiler's sphere of checking.

## A Bounding Measurement

The run above showed that the chosen property holds for **every** input — but that outcome
comes from the chosen property being a true mathematical fact: reversing an array twice
always gives back the original array, regardless of order. Not every property is like this.
The run below tests, with the same generator, a property that holds for some inputs but not
others: an array being equal to its own reverse, that is, a palindrome condition.

```rust
// bounding_test.rs — a property that holds for some inputs, not for others
mod linear_generator;
use linear_generator::LinearGenerator;

fn equals_own_reverse(v: &[i32]) -> bool {
    let mut reversed = v.to_vec();
    reversed.reverse();
    reversed == v
}

fn main() {
    let mut generator = LinearGenerator::new(7);
    let total_inputs = 200;
    let mut held = 0;
    let mut first_non_holding: Option<Vec<i32>> = None;

    for _ in 0..total_inputs {
        let input = generator.vector(6);
        if equals_own_reverse(&input) {
            held += 1;
        } else if first_non_holding.is_none() {
            first_non_holding = Some(input);
        }
    }

    println!("inputs tested: {}", total_inputs);
    println!("inputs the property held for: {}", held);
    println!("first non-holding input length: {}", first_non_holding.map(|v| v.len()).unwrap_or(0));
}
```

```
inputs tested: 200
inputs the property held for: 62
first non-holding input length: 3
```

**PL7.** The property held for sixty-two of the two hundred inputs, and failed to hold for
the remaining hundred thirty-eight — because the generator can also produce short arrays (at
most six elements, including zero), some inputs turned out palindromic by chance: an empty
array and a one-element array are always equal to their own reverse, and this raises the
holding count. The first non-holding input's length is three — a three-element array being
a palindrome depends on the elements themselves, not on its length.

This measurement limits what a passing property test means. In the previous run, the
property held for all two hundred of two hundred inputs, because the tested property was
*always* a true mathematical fact; in this run, the property held for only sixty-two inputs,
because the tested property is *sometimes* true. The test itself does not show the
difference between the two — both look like a "passing test," except that the count of
passing inputs is not entirely equal. A property test passing proves that the property holds
for the tested set of inputs; it says nothing about an input that was not tested. Two
hundred inputs may be a wide sample, but the generator draws from a bounded range (`-500` to
`499`, at most twenty elements) — an input outside this range, or beyond this length, was
never tested at all. **PL8.** The guarantee a passing test gives is bounded by the number
and range of inputs it tested; when a claim is written without being tied to that number,
the guarantee's real scope is hidden.

The measurement carries one more limit: the first non-holding input was reported only by its
length, not by its elements — this is not a choice made because of the environment, it is a
deliberate one. Real property-based testing tools, once they find a non-holding input,
reduce it to a smaller, more readable counterexample through a process called *shrinking*;
this lesson does not write such a shrinking step, it only reports the first non-holding
input's length. This is another limit of the measurement: showing that a property does not
hold is not the same as finding the most *understandable* counterexample — what is counted
in this lesson is only how many inputs it holds for, not which input is the most
illustrative counterexample.

The number sixty-two itself also carries a limit: it is the count these particular two
hundred inputs, produced with seed `7`, gave. Had seed `11` or `100` been chosen, the count
of holding inputs would have come out different even with the same property tested across
the same range — because the set of produced inputs changes depending on the seed. This
lesson does not measure that variability; with a single seed, it makes a single run
deterministic and reports that run's result. Determinism here guarantees repeatability (the
same seed always gives the same sixty-two), but it does not guarantee the tested range's
*representativeness* — that strength depends on how the range and the generator are
designed, not on a single run's result.

## Summary

- Rust's standard library has no random-number generator; this lesson used a fixed-seed,
  hand-written linear congruential generator — the same seed produces the same input
  sequence on every run.
- The property that applying reversal twice gives identity held for all two hundred of the
  two hundred generated inputs.
- The guarantee's class is **handed to the writer**: the compiler checked the test code's
  type and stopped; the writer decided which property to test, over which input range, and
  with how many inputs.
- A property that is sometimes true (being equal to its own reverse), tested with the same
  generator, held for only sixty-two of the two hundred inputs — a passing test does not
  mean correct for every input.
- This measurement's limit: the guarantee a property test gives extends only as far as the
  number and range of tested inputs; an input outside the range was never tested at all.
- The number of inputs, their range, and the tested property itself — including how much of
  each is enough — are decisions made by the party who writes the test, not the compiler;
  the same three parts (generator, property, counter) apply to bodies other than reversal,
  too.

## Next Step

This lesson measured that we can show, *by running*, that a property runs correctly, but
that this demonstration stays bounded by a limited number of inputs and the writer's
decisions. The next lesson applies the same "guarantee established by running" idea to a
completely different question: which of two implementations does less work can also only be
learned by running, a question the compiler never asks, just like correctness. But this time
what is tested is not correctness, it is operation count; and instead of a number of inputs,
this time it is how many comparisons and how many copies happen on a single input that gets
counted. The Benchmarking and Profiling lesson examines what this number *does* and does not
say — specifically, why not a duration.
