← Back to list

TypeScript + fast-check Property Tests: Prove Invariants Without a Mock Jungle

Replace brittle example-based tests with property tests that generate thousands of inputs, shrink failures, and validate your real…

Nikulsinh Rajput · 2026-01-06 03:32 · 56 claps · 4.6 min read paywalled
#typescript #testing #property-based-testing #software-engineering #javascript
Open on Medium ↗
Wiki topics: 🌐 · Web Development

TypeScript + fast-check Property Tests: Prove Invariants Without a Mock Jungle

Replace brittle example-based tests with property tests that generate thousands of inputs, shrink failures, and validate your real invariants — without drowning in mocks.

Use TypeScript + fast-check property testing to prove invariants at scale, auto-generate edge cases, shrink failures, and cut flaky mock-heavy tests.

You know that moment when you open a test file and it’s 70% mocks, 20% setup, and 10% actual assertions… and still the bug escapes to production? Let’s be real: we’ve all built a mock jungle at least once. It feels productive right up until the day you realize you’re testing your mocks more than your code.

Property-based testing is the exit door.

With fast-check in TypeScript, you don’t hand-pick a few “representative” examples. You define properties — invariants that must always be true — and let the library generate hundreds or thousands of test cases, including the weird edge cases you didn’t think of.

And when it fails? fast-check shrinks the input to the smallest counterexample. That alone is worth the price of admission.

Why example-based tests hit a ceiling (and mocks fill the gap)

Classic unit tests are great at documenting specific scenarios:

  • “when input is X, output is Y”
  • “when user is premium, feature is enabled”
  • “when API returns 500, show retry UI”

But here’s what they’re bad at:

  • combinatorial input spaces (strings, dates, nested objects, partials)
  • edge conditions (empty, huge, unicode, duplicates, ordering)
  • invariants that should hold for all valid inputs

So what happens? You either:

  1. write a few examples and hope, or
  2. try to simulate reality with mocks until your tests become fragile theatre

Property tests flip it: you keep the test simple and push complexity into input generation.

Property testing in one sentence

Instead of testing examples, you test laws.

Think of it like physics: you’re not proving gravity with a single apple. You’re stating a rule and throwing a thousand apples.

The setup: fast-check + your test runner

Most teams pair fast-check with Jest or Vitest. The pattern is the same.

npm i -D fast-check
# plus your runner
npm i -D vitest

A typical Vitest file:

import { describe, it, expect } from "vitest";
import fc from "fast-check";

That’s it. No special runner required.

Start small: invariants that pay off immediately

1) “Round-trip” properties (encode/decode, serialize/parse)

If you have any serialization logic — URLs, tokens, query strings, JSON transforms — round-trip properties are gold.

Let’s say you build a tiny querystring encoder:

export function encode(params: Record<string, string>): string {
  const usp = new URLSearchParams(params);
  return usp.toString();
}

export function decode(qs: string): Record<string, string> {
  const usp = new URLSearchParams(qs);
  return Object.fromEntries(usp.entries());
}

Property: decoding what you encode should give you the same map.

it("encode/decode round-trip", () => {
  fc.assert(
    fc.property(
      fc.dictionary(fc.string(), fc.string()),
      (params) => {
        const qs = encode(params);
        const decoded = decode(qs);
        expect(decoded).toEqual(params);
      }
    )
  );
});

You might be thinking, “this looks too easy.” Exactly. The power is in the generated inputs:

  • empty keys
  • unicode
  • repeated patterns
  • long strings

If your assumption breaks, you’ll know fast.

The secret weapon: shrinking

When a property fails, fast-check doesn’t just say “failed for some random huge object.” It tries to shrink the failing input into the smallest case that still fails.

That’s the opposite of mock jungles. Instead of expanding complexity, it compresses it.

It’s like the library is saying: “Here. This one tiny input breaks your invariant. Fix that.”

A real-world example: “no duplicates” order processing

Imagine you process an order, and your invariant is:

  • the output should contain no duplicate item IDs
  • it should preserve first occurrence order (so UX stays stable)

Here’s a function:

export function uniqueStable(ids: string[]): string[] {
  const seen = new Set<string>();
  const out: string[] = [];
  for (const id of ids) {
    if (!seen.has(id)) {
      seen.add(id);
      out.push(id);
    }
  }
  return out;
}

Now properties:

  1. output has no duplicates
  2. output is a subsequence of input
  3. applying twice does nothing (idempotent)
it("uniqueStable invariants", () => {
  fc.assert(
    fc.property(fc.array(fc.string()), (ids) => {
      const out = uniqueStable(ids);

      // 1) no duplicates
      expect(new Set(out).size).toBe(out.length);

      // 2) subsequence (order preserved)
      let j = 0;
      for (const x of ids) if (j < out.length && out[j] === x) j++;
      expect(j).toBe(out.length);

      // 3) idempotent
      expect(uniqueStable(out)).toEqual(out);
    })
  );
});

No mocks. No fixtures. And you just tested thousands of cases in one shot.

Where property tests kill the mock jungle

Mocks usually appear when you test behavior tied to “the world”:

  • databases
  • time
  • randomness
  • network calls

Property tests shine when you separate pure logic from effects.

The practical architecture shift

[Effects] -----> [Pure core] -----> [Effects]
 DB/HTTP            rules            logging

You property-test the “pure core” with generated inputs. You integration-test the effects with a smaller number of examples.

This split is what stops you from mocking half the internet.

Advanced pattern: model-based testing (tiny spec, brutal coverage)

Let’s say you have a cart reducer:

  • add item
  • remove item
  • clear cart

Instead of writing 30 scenario tests, define a simple model (like a JS Map) and assert your implementation matches it for random command sequences.

Pseudo-structure:

type Cmd =
  | { t: "add"; id: string; qty: number }
  | { t: "remove"; id: string }
  | { t: "clear" };

function runModel(cmds: Cmd[]) { /* reference behavior */ }
function runImpl(cmds: Cmd[]) { /* your real reducer */ }

Property: for any sequence of commands, model and implementation end up equal.

fast-check supports command-based testing patterns like this, and it’s ridiculously effective for reducers, state machines, caches, and parsers.

Practical tips so your suite stays fast and readable

1) Limit input sizes (on purpose)

Generated tests can get heavy. Start with constraints:

fc.array(fc.string(), { minLength: 0, maxLength: 200 })

2) Seed your failures

If CI fails, you want to reproduce locally. fast-check prints a seed; you can rerun with it.

3) Use pre conditions carefully

If your property only applies to valid inputs, filter or precondition:

fc.pre(user.age >= 0);

But don’t overuse it. Too many preconditions can reduce coverage.

4) Combine with a few example tests

Property tests prove laws. Example tests document specific business cases. You want both.

Conclusion: test the truth, not the theatre

If your test suite feels like a performance — lots of props, lots of staging, fragile outcomes — it’s probably time for property tests.

With TypeScript + fast-check, you can:

  • encode invariants as executable laws
  • generate edge cases automatically
  • get minimal counterexamples via shrinking
  • keep your tests focused on behavior, not mock choreography

Try it on one pure function this week: a parser, a reducer, a “dedupe,” a validator. Then watch how quickly you start seeing places where “examples” were never going to be enough.

CTA: If you share one function you’re currently drowning in mocks to test (even a simplified version), I’ll suggest 2–3 properties to validate it and a fast-check generator that fits.


메타데이터
post_id
15ac4401d09d
slug
typescript-fast-check-property-tests-prove-invariants-without-a-mock-jungle-15ac4401d09d
url
https://medium.com/@hadiyolworld007/typescript-fast-check-property-tests-prove-invariants-without-a-mock-jungle-15ac4401d09d
canonical_url
https://medium.com/@hadiyolworld007/typescript-fast-check-property-tests-prove-invariants-without-a-mock-jungle-15ac4401d09d
author_url
https://medium.com/@hadiyolworld007
status
ok
fetched_at
2026-06-27 07:40:21