# Property-Based Testing

Course: [Testing & Verification](https://www.devtutorials.ai/learn/testing-verification) · Section: Verifying AI Changes
Source: https://www.devtutorials.ai/learn/testing-verification/verifying-ai-changes/property-based-testing

> Write rules that must hold for every input, let fast-check try hundreds of random inputs, and watch shrinking find the smallest one that breaks the AI's code. Bounds, sums, round trips and no-change properties.

## What a Property Is

So far, every test checked one example: "10% off ₹500 is ₹450".

Examples are good. But you only test the inputs you thought of. AI code often breaks on the inputs nobody thought of.

**Property** = a rule that must be true for **every** input, not just one.

For example:

- A discount never makes the price negative.
- A discount never makes the price bigger.
- The shares of a split bill always add up to the total.

**Property-based testing** = a tool makes hundreds of random inputs and checks the rule on each one.

**Simple lesson:** an example checks one input. A property checks a rule on hundreds.

### A Real-World Example

Think about testing a new pressure cooker.

- An example test: cook 1 cup of rice once. It works.
- A property test: "the lid never opens while there is pressure". You try it with rice, dal, water, a full pot, a nearly empty pot, many times.
- One day, with a nearly empty pot, the lid opens. You found a case nobody planned for.

Property tests are the "many times, many ways" check.

## What You Will Learn

In this lesson, you will learn:

1. How to write a property with **fast-check**.
2. How **shrinking** finds the smallest input that breaks a rule.
3. Four kinds of good properties: bounds, sums, round trips and "no change".
4. How properties and examples work together.
5. How to tell a real property from one that just copies the code.

## Your First Property

### The Parts of a Property Test

A property test has three parts:

1. **Generators** say what random inputs to make. For example, `fc.integer({ min: 0, max: 100 })`.
2. **The rule** is a function that returns `true` when the rule holds.
3. **`fc.assert`** runs the rule on many inputs. It fails if any input breaks the rule.

Press **▶ Run tests**. Green.

- **What happened:** the tool made 100 random prices and percents, and the rule held for all of them.
- **Now try:** open `src/discount.ts` and change `100 - percent` to `100 + percent`. Run again. Red.

**This lab uses a small copy of fast-check.**

- Real projects install `fast-check` from npm. The code you write is the same.
- `fc.float2` is a lab helper for money with 2 decimals, like 12.34. In real fast-check, you can make it with `fc.integer(...).map((p) => p / 100)`.
- Runs use a fixed **seed**, so you get the same inputs every time.

**Simple lesson:** generators, a rule, and `fc.assert`. That is a property test.

## Shrinking

### The Smallest Failing Input

When a property fails, the first failing input is often big and messy, like ₹58,230.76.

**Shrinking** = the tool tries smaller and simpler inputs, and keeps the smallest one that still fails.

<figure className="ver-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="ver-fig-shrink" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="ver-fig-shrink">Animation: a failing price of 58230.76 is halved again and again: 29115.38, 909.86, 7.11, 0.45, until it reaches 0.01, the smallest input that still fails.</title>
  <g style={{ fontFamily: "var(--mono)", fontSize: 13 }}>
    <rect x="20" y="40" width="110" height="40" rx="8" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }} /><text x="75" y="65" textAnchor="middle" style={{ fill: "var(--ink)" }}>58230.76</text>
    <rect x="145" y="40" width="110" height="40" rx="8" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }} opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.15;0.2;1" dur="8s" repeatCount="indefinite" /></rect>
    <text x="200" y="65" textAnchor="middle" style={{ fill: "var(--ink)" }} opacity="0">909.86<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.15;0.2;1" dur="8s" repeatCount="indefinite" /></text>
    <rect x="270" y="40" width="110" height="40" rx="8" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }} opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.35;0.4;1" dur="8s" repeatCount="indefinite" /></rect>
    <text x="325" y="65" textAnchor="middle" style={{ fill: "var(--ink)" }} opacity="0">7.11<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.35;0.4;1" dur="8s" repeatCount="indefinite" /></text>
    <rect x="395" y="40" width="110" height="40" rx="8" style={{ fill: "var(--ver-red-soft)", stroke: "var(--ver-red)" }} opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.55;0.6;1" dur="8s" repeatCount="indefinite" /></rect>
    <text x="450" y="65" textAnchor="middle" style={{ fill: "var(--ink)" }} opacity="0">0.45<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.55;0.6;1" dur="8s" repeatCount="indefinite" /></text>
    <rect x="520" y="40" width="100" height="40" rx="8" style={{ fill: "var(--ver-amber-soft)", stroke: "var(--ver-amber)", strokeWidth: 2.5 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.75;0.8;1" dur="8s" repeatCount="indefinite" /></rect>
    <text x="570" y="65" textAnchor="middle" style={{ fill: "var(--ink)", fontWeight: 700 }} opacity="0">0.01<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.75;0.8;1" dur="8s" repeatCount="indefinite" /></text>
  </g>
  <text x="320" y="120" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>Each step tries a smaller input. It keeps it only if the rule still fails.</text>
  <text x="570" y="105" textAnchor="middle" style={{ fill: "var(--ver-amber)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }} opacity="0">smallest!<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.8;0.85;1" dur="8s" repeatCount="indefinite" /></text>
</svg>
<figcaption>Shrinking turns a messy failure into the simplest one you can debug.</figcaption>
</figure>

Press **▶ Run tests**.

- **The Tests tab:** red. Read the message: "Counterexample: [0.01]", then every shrink step.
- **Counterexample** = an input that breaks the rule.
- **Why it fails:** the code rounds to whole rupees. ₹0.01 becomes ₹0.
- **Fix it:** change the last line to `Math.round(price * (100 - percent)) / 100`. Run again. Green.

**Simple lesson:** shrinking hands you the simplest broken case. Start debugging there.

## Kinds of Good Properties

Good properties describe the **result**, without repeating the formula. Here are four kinds that work on most code.

### Bounds

**Bounds** = the result always stays inside a range.

- A discount is between 0 and the price.
- A rating average is between 1 and 5.

### Sums and Counts

The parts add up to the whole, and nothing is lost.

Press **▶ Run tests**.

- **The Tests tab:** red. "Counterexample: [0.01, 2]". ₹0.01 split between 2 people loses the paisa.
- **Why:** this version of `splitBill` drops the extra paise.
- **A person might never try ₹0.01 for 2 people.** The tool tried it for you.

### Round Trips

**Round trip** = going there and back gives the same value. For example, format a number as text, then read it back.

Press **▶ Run tests**. Green.

- **Now try:** change `toFixed(2)` to `toFixed(1)` in `src/money.ts`. Run again.
- **The Tests tab:** red, with a small counterexample like ₹0.01.

### No Change

Some inputs should change nothing.

- 0% off keeps the price.
- Sorting a list that is already sorted gives the same list.

**Do not copy the code into the property.**

- `applyDiscount(p, x) === Math.round(p * (100 - x)) / 100` repeats the formula.
- If the formula is wrong, the property is wrong in the same way.
- Describe what must be true of the result instead: bounds, sums, round trips, no change.

**Simple lesson:** a good property says what the result must look like, not how to compute it.

## Properties Plus Examples

Properties and examples work best together.

- **Examples** pin exact answers: "10% off ₹500 is ₹450".
- **Properties** guard the rules across many inputs, including odd ones.

Press **▶ Run tests**, then **🐞 Plant bugs**.

- **The Bug check tab:** one example plus one property catch all five discount bugs.
- **Why:** the example catches wrong maths. The 0% property catches special cases and rounding.

**Where properties help most with AI code:**

- Money and rounding.
- Lists: sorting, splitting, merging.
- Text: format, parse, trim.
- Any rule with "never", "always" or "adds up to".

**Simple lesson:** examples check what you know. Properties find what you did not think of.

## TL;DR

1. **Property** = a rule that must hold for every input.
2. **Generator** = says what random inputs to make, like `fc.integer({ min: 0, max: 100 })`.
3. **`fc.assert(fc.property(...))`** = runs the rule on many random inputs.
4. **Counterexample** = an input that breaks the rule.
5. **Shrinking** = finding the smallest input that still breaks it.
6. **Good kinds of property:**
    - bounds
    - sums and counts
    - round trips
    - no change
7. **Do not copy the formula** into the property.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Copying the formula | "The property is just the code again." | Describe the result: bounds, sums, round trips. | A wrong formula passes its own check. |
| Rule too weak | "It returns a number." | Pick a rule a bug would break. | Every bug still returns a number. |
| Rule too strict | "All shares are equal." | Check what is really always true. | The test fails on correct code. |
| Ignoring the counterexample | "It is a random fluke." | It is a real input that breaks the rule. | The bug ships for that input. |
| Only properties | "Properties replace examples." | Use both. | Exact answers stop being checked. |

## Check Your Understanding

## Exercises

## What to Remember

- A property is a rule for every input. The tool makes the inputs.
- Shrinking gives you the smallest failing case.
- Good properties: bounds, sums and counts, round trips, no change.
- Never copy the formula into the property.
- Use properties and examples together.

## Further Reading

- [fast-check documentation](https://fast-check.dev/docs/introduction/): the real tool, with every generator.
- [Vitest getting started](https://vitest.dev/guide/): run property tests with your other tests.

Next: **Integration and Contract Tests**. A mocked unit test stays green while the real API changes. You will add a contract test that catches it.
