fast-check-testing
Authors property-based tests in JavaScript / TypeScript using fast-check - wires `fc.assert(fc.property(arbitrary, ...))`, picks arbitraries (`fc.integer`, `fc.string`, `fc.array`, `fc.tuple`, `fc.record`), uses `.map()` / `.chain()` / `.filter()` to build domain arbitraries, and integrates with Jest / Vitest / Mocha / Jasmine / AVA / Tape. Use when a JS/TS codebase needs PBT to catch edge cases - fast-check has been used to find bugs in major libraries (`query-string`, etc.) and is trusted by Jest, Jasmine, fp-ts, Ramda.
Install with skills.sh (any agent)
npx skills add testland/qa --skill fast-check-testingfast-check-testing
Overview
fast-check is "a property-based testing framework for JavaScript and TypeScript, inspired by QuickCheck" (fast-check-readme (opens in new window)).
It's runner-agnostic (fast-check-overview (opens in new window)): "Test Runner Agnostic: Works seamlessly with Jest, Mocha, Vitest, and other testing frameworks without special integration."
When to use
Install
Per fast-check-readme (opens in new window):
npm install fast-check --save-dev
# or
yarn add fast-check --dev
# or
pnpm add -D fast-checkBasic property
Per fast-check-readme (opens in new window), the canonical Mocha-style example:
import fc from 'fast-check';
const contains = (text, pattern) => text.indexOf(pattern) >= 0;
describe('properties', () => {
it('should always contain itself', () => {
fc.assert(fc.property(fc.string(), (text) => contains(text, text)));
});
it('should always contain its substrings', () => {
fc.assert(
fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {
return contains(a + b + c, b);
})
);
});
});The shape: fc.assert(fc.property(<arbitraries...>, (...inputs) => <predicate>)).
The predicate returns:
fc.assert runs the property with 100 generated cases by default; on failure, fast-check shrinks to the minimal counterexample.
Arbitraries and composite inputs
Pick arbitraries that match the input domain, build domain values with .map() / .chain(), and assemble objects with fc.record. Full catalog and combinator patterns: references/arbitraries.md. Prefer constrained arbitraries (fc.integer({ min: 1 }), fc.emailAddress()) over broad ones filtered down.
Integrate with the test runner
Per fast-check-overview (opens in new window): works "with major testing frameworks including Jest, Vitest, Mocha, Jasmine, AVA, and Tape" without special integration.
// Jest / Vitest example
import { test, expect } from 'vitest';
import fc from 'fast-check';
test('reverse is involutive', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
expect([...arr].reverse().reverse()).toEqual(arr);
})
);
});The assertion library (expect, assert) is the runner's; fast-check hooks into thrown errors as failures. For async properties use await fc.assert(fc.asyncProperty(...)).
Shrinking and reproducibility
On failure fast-check prints the shrunk counterexample plus a seed; replaying that seed reproduces the failure deterministically, and a fixed CI seed keeps runs stable. Output format, replay, and CI seed: references/shrinking-and-reproducibility.md.
Stateful and async testing
For concurrent code use fc.scheduler race-condition detection; for stateful systems use command-sequence model-based testing with fc.commands / fc.modelRun. Both patterns: references/stateful-and-async.md.
How to use
Worked example
A codec exposes encode(obj) and decode(str) and claims decode(encode(x)) === x for every payload.
import fc from 'fast-check';
import { encode, decode } from './codec';
const payload = fc.record({
id: fc.uuid(),
count: fc.integer({ min: 0 }),
tags: fc.uniqueArray(fc.string()),
});test('decode reverses encode', () => {
fc.assert(
fc.property(payload, (x) => {
expect(decode(encode(x))).toEqual(x);
})
);
});Property failed after 12 tests
{ seed: 42, path: "11:0", endOnFailure: true }
Counterexample: [{"id":"...","count":0,"tags":[""]}]Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Random CI seed | Property fails on CI, passes locally; hard to reproduce. | Fixed seed in CI (shrinking-and-reproducibility.md). |
Heavy .filter() on broad arbitraries | Generation slow; many cases discarded. | Constrained arbitraries (arbitraries.md). |
| Asserting on specific values inside the property | Defeats PBT; that's an example test. | Properties assert relationships; examples go elsewhere. |
| Mocking dependencies inside the property | Mocks don't satisfy properties. | Test pure functions; integration tests for the rest. |
fc.assert(fc.property(...)) without await for async props | Test passes incorrectly (Promise rejected silently). | await fc.assert(fc.asyncProperty(...)). |
Generating an fc.string() for an email field | Wastes generation budget; mostly invalid. | fc.emailAddress() (arbitraries.md). |
| One mega-property that asserts 5 things | When it fails, hard to know which thing. | One property per logical assertion. |
Limitations
References
fast-check arbitraries, combinators, and composite inputs
View source (opens in new window)fast-check arbitraries, combinators, and composite inputs
How to build the generators passed to fc.property(<arbitraries...>, predicate).
Arbitraries catalog
Per fast-check-overview (opens in new window):
| Arbitrary | Generates |
|---|---|
fc.string() | Strings |
fc.integer() | Integers |
fc.float() / fc.double() | Floats |
fc.boolean() | Booleans |
fc.array(item) | Arrays of item |
fc.tuple(a, b, ...) | Fixed-length tuples |
fc.record({ key: ... }) | Objects with specified properties |
fc.option(item) | item or null |
fc.constantFrom(...) | One of fixed values |
fc.oneof(a, b, ...) | One of multiple arbitraries |
fc.uuid() / fc.ipV4() / fc.emailAddress() / fc.webUrl() | Format-specific |
fc.date() | Dates |
fc.uniqueArray(item) | Arrays without duplicates |
fc.dictionary(key, value) | Map / Record types |
Combinators (.map / .chain / .filter)
Per fast-check-overview (opens in new window): "Extensible via map() and chain() combinators."
// .map: transform a generated value
const evenInteger = fc.integer().map(n => n * 2);
// .chain: dependent generation (later value depends on earlier)
const stringWithKnownLength = fc.integer({ min: 1, max: 100 })
.chain(len => fc.string({ minLength: len, maxLength: len }));
// .filter: reject (use sparingly - slow when filter rejects most)
const positiveInteger = fc.integer().filter(n => n > 0);
// Better:
const positiveInteger = fc.integer({ min: 1 });.filter() discards rejections; .map() transforms. Prefer .map() and constrained arbitraries over .filter() when possible.
Composite arbitraries via fc.record
const user = fc.record({
id: fc.uuid(),
email: fc.emailAddress(),
age: fc.integer({ min: 18, max: 100 }),
tags: fc.uniqueArray(fc.constantFrom('admin', 'beta', 'churn-risk')),
createdAt: fc.date({ min: new Date('2020-01-01'), max: new Date() }),
});
it('serializes user to JSON and back', () => {
fc.assert(
fc.property(user, (u) => {
expect(JSON.parse(JSON.stringify(u))).toEqual({
...u,
createdAt: u.createdAt.toISOString(),
});
})
);
});fc.record produces objects with the specified shape; each field is sampled per its arbitrary.
fast-check shrinking and reproducibility
View source (opens in new window)fast-check shrinking and reproducibility
When a property fails, fast-check prints the falsifying input + a shrunk minimal version + a seed:
Property failed after 47 tests
{ seed: 1234567890, path: "12:1:0", endOnFailure: true }
Counterexample: [{"id": "abc", "age": -1}]
Shrunk 8 time(s)
Got error: Expected age to be >= 18, got -1To reproduce, replay with the seed:
fc.assert(
fc.property(...),
{ seed: 1234567890, path: "12:1:0", endOnFailure: true }
);The seed/path is the deterministic recipe to re-derive the failure.
For CI, set a fixed seed:
import fc from 'fast-check';
fc.configureGlobal({ seed: process.env.CI ? 42 : Date.now() });fast-check stateful and async testing
View source (opens in new window)fast-check stateful and async testing
Advanced fast-check for concurrent and stateful systems.
Race condition detection
Per fast-check-overview (opens in new window): "Race condition detection for async code."
import { test } from 'vitest';
import fc from 'fast-check';
test('concurrent counter increments are atomic', async () => {
await fc.assert(
fc.asyncProperty(fc.scheduler(), async (s) => {
const counter = new AsyncCounter();
const tasks = [
s.schedule(counter.increment()),
s.schedule(counter.increment()),
s.schedule(counter.increment()),
];
await s.waitAll();
await Promise.all(tasks);
expect(counter.value).toBe(3);
})
);
});fc.scheduler exhaustively explores task interleavings; s.schedule queues an async operation; s.waitAll() advances. fast-check finds interleavings that cause the property to fail - the canonical race-condition catcher.
Model-based testing
Per fast-check-overview (opens in new window): "Model-based testing for stateful systems."
class CounterModel {
count = 0;
increment() { this.count++; }
decrement() { this.count--; }
}
const allCommands = [
fc.constant({ run: (c, real) => { c.increment(); real.increment(); expect(real.value).toBe(c.count); } }),
fc.constant({ run: (c, real) => { c.decrement(); real.decrement(); expect(real.value).toBe(c.count); } }),
];
it('counter behaves per model', () => {
fc.assert(
fc.property(fc.commands(allCommands), (cmds) => {
const model = new CounterModel();
const real = new RealCounter();
fc.modelRun(() => ({ model, real }), cmds);
})
);
});fast-check generates random sequences of commands; the model stays in sync with the real implementation; any divergence is a bug in the real implementation.
Related skills
hypothesis-testing
Authors property-based tests in Python using Hypothesis - wires `@given` with `strategies` (`st.integers`, `st.text`, `st.lists`, `st.from_regex`, `st.composite`), uses `assume()` / `.filter()` for preconditions, configures via `@settings(max_examples=..., deadline=...)`, and exploits Hypothesis's automatic shrinking to find the falsifying example. Integrates with pytest fixtures + parametrize. Use when a Python project needs PBT to catch edge cases the example-based tests miss - bug clusters around input ranges / boundary values / interaction between fields.
jqwik-testing
Authors property-based tests for the JVM (Java + Kotlin) using jqwik - wires `@Property` test methods, `@ForAll` parameter annotations, `Arbitraries.integers/strings/etc` generators, custom `@Provide` arbitraries, and the JUnit 5 platform integration. Use when a JVM project needs PBT - alternative to JUnit-QuickCheck and Vavr's property-checking; tightly integrates with JUnit 5 so property tests run alongside conventional unit tests in the same Maven / Gradle pipeline.
proptest-testing
Authors property-based tests in Rust using proptest - wires the `proptest!` macro, defines strategies (`prop::collection::vec`, type-driven `any` strategies, regex-based string strategies), uses the strategy-per-value model (vs QuickCheck's per-type) for flexible composition, and exploits proptest's automatic shrinking + persistence of failed cases (regression test artifact). Use when a Rust codebase needs PBT - pairs especially well with parsers, serializers, and any function with a structured input domain.
quickcheck-testing
Authors property-based tests for Haskell using QuickCheck (the original PBT library) and for Scala via ScalaCheck (the JVM port) - wires `quickCheck` (Haskell) / `forAll` (ScalaCheck) drivers, defines `Arbitrary` instances or generators, uses `shrink` to find minimal counterexamples, and integrates with HSpec / Tasty (Haskell) or specs2 / ScalaTest. Use when the codebase is Haskell or Scala and the team wants the canonical PBT library that the entire family (Hypothesis / fast-check / proptest / jqwik) was inspired by.