fast-check: Property-Based Testing for JavaScript and TypeScript
fast-check is the leading property-based testing library for JavaScript and TypeScript. It generates test inputs automatically, shrinks failures, and integrates with Jest, Vitest, and Mocha with zero configuration changes. If you write JavaScript and want to find bugs that hand-written tests miss, fast-check is the tool.
Installation
npm install --save-dev fast-checkWorks with Jest, Vitest, Mocha, and any other test runner — fast-check doesn't depend on or replace your runner.
Basic Usage
import fc from 'fast-check';
test('string concatenation length', () => {
fc.assert(
fc.property(fc.string(), fc.string(), (a, b) => {
return (a + b).length === a.length + b.length;
})
);
});
test('array reverse is idempotent', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const reversed = [...arr].reverse();
const doubleReversed = [...reversed].reverse();
expect(doubleReversed).toEqual(arr);
})
);
});fc.assert runs the property with 100 randomly generated examples by default. If any fail, it shrinks the input to the smallest failing case and throws with a clear error.
Core Arbitraries
Arbitraries describe the space of generated values:
// Primitives
fc.integer() // any integer
fc.integer({ min: 0, max: 100 }) // bounded
fc.float() // any float
fc.float({ min: 0, max: 1, noNaN: true })
fc.boolean()
fc.string()
fc.string({ minLength: 1, maxLength: 50 })
fc.constant('fixed value') // always returns this
// Collections
fc.array(fc.integer())
fc.array(fc.integer(), { minLength: 1, maxLength: 10 })
fc.set(fc.integer()) // unique values
fc.dictionary(fc.string(), fc.integer())
fc.tuple(fc.integer(), fc.string()) // fixed-length, typed
// Specialized
fc.uuid()
fc.emailAddress()
fc.webUrl()
fc.ipV4()
fc.date()
fc.fullUnicodeString()
fc.base64String()
// Union types
fc.oneof(fc.integer(), fc.string())
fc.option(fc.integer()) // integer | nullTypeScript-First Design
fast-check is written in TypeScript and provides full type inference:
// Types flow through automatically
const userArbitrary = fc.record({
id: fc.uuid(),
name: fc.string({ minLength: 1, maxLength: 100 }),
age: fc.integer({ min: 0, max: 150 }),
email: fc.emailAddress(),
active: fc.boolean(),
});
// TypeScript knows the type: { id: string, name: string, age: number, ... }
fc.assert(
fc.property(userArbitrary, (user) => {
const serialized = JSON.stringify(user);
const deserialized = JSON.parse(serialized);
expect(deserialized.id).toBe(user.id);
expect(deserialized.name).toBe(user.name);
})
);Composing Arbitraries
fc.record
Build object arbitraries with typed fields:
const orderItemArbitrary = fc.record({
productId: fc.string({ minLength: 1 }),
quantity: fc.integer({ min: 1, max: 99 }),
unitPrice: fc.float({ min: 0.01, max: 9999.99, noNaN: true }),
});fc.chain (dependent arbitraries)
Generate values that depend on previously generated values:
// Generate a list and a valid index
const listAndIndex = fc.array(fc.integer(), { minLength: 1 }).chain(
(arr) => fc.tuple(fc.constant(arr), fc.integer({ min: 0, max: arr.length - 1 }))
);
fc.assert(
fc.property(listAndIndex, ([arr, idx]) => {
expect(arr[idx]).toBeDefined(); // index is always valid
})
);fc.filter
Constrain the generated space:
const positiveEven = fc.integer({ min: 1 }).filter((n) => n % 2 === 0);
// Prefer bounded arbitraries over heavy filtering
const positiveEven2 = fc.integer({ min: 1 }).map((n) => n * 2); // fasterfc.map
Transform generated values:
const sortedArrays = fc.array(fc.integer()).map((arr) => [...arr].sort((a, b) => a - b));
const isoDateStrings = fc.date().map((d) => d.toISOString());Properties and Assertions
Three ways to express properties:
// Return boolean (classic style)
fc.assert(
fc.property(fc.integer(), (n) => {
return n * 2 === n + n; // true = pass, false/throw = fail
})
);
// Use expect (Jest/Vitest style)
fc.assert(
fc.property(fc.string(), (s) => {
expect(s.toLowerCase().toUpperCase()).toBe(s.toUpperCase());
})
);
// Throw on failure
fc.assert(
fc.property(fc.array(fc.integer(), { minLength: 1 }), (arr) => {
const max = Math.max(...arr);
if (!arr.includes(max)) throw new Error('Max not in array');
})
);Async Properties
test('API round trip', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
title: fc.string({ minLength: 1, maxLength: 200 }),
body: fc.string({ maxLength: 5000 }),
}),
async (post) => {
const created = await api.createPost(post);
const retrieved = await api.getPost(created.id);
expect(retrieved.title).toBe(post.title);
expect(retrieved.body).toBe(post.body);
}
),
{ numRuns: 20 } // fewer runs for integration tests
);
});Model-Based Testing
Test stateful systems by modeling expected behavior alongside the real implementation:
import fc from 'fast-check';
// Model (simple reference implementation)
class CartModel {
items: Map<string, number> = new Map();
addItem(id: string, qty: number) {
this.items.set(id, (this.items.get(id) ?? 0) + qty);
}
removeItem(id: string) {
this.items.delete(id);
}
total(prices: Map<string, number>) {
let sum = 0;
for (const [id, qty] of this.items) {
sum += (prices.get(id) ?? 0) * qty;
}
return sum;
}
}
// Commands
class AddItemCommand implements fc.Command<CartModel, Cart> {
constructor(readonly id: string, readonly qty: number) {}
check() { return this.qty > 0; }
run(model: CartModel, real: Cart) {
model.addItem(this.id, this.qty);
real.addItem(this.id, this.qty);
expect(real.itemCount()).toBe(model.items.size);
}
toString() { return `AddItem(${this.id}, ${this.qty})`; }
}
test('cart model matches implementation', () => {
const idArb = fc.string({ minLength: 1, maxLength: 10 });
const qtyArb = fc.integer({ min: 1, max: 10 });
const commandsArb = fc.array(
fc.oneof(
fc.record({ id: idArb, qty: qtyArb }).map(({ id, qty }) => new AddItemCommand(id, qty)),
)
);
fc.assert(
fc.property(commandsArb, (commands) => {
const model = new CartModel();
const real = new Cart();
fc.modelRun(() => ({ model, real }), commands);
})
);
});Shrinking in Action
When fast-check finds a failure with a large input, it automatically reduces it:
test('no integer overflow', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
return safeAdd(a, b) === a + b; // will fail for large numbers
})
);
});
// Output when it fails:
// Property failed after 1 tests
// { seed: 12345, path: "0", endOnFailure: true }
// Counterexample: [2147483647, 1] ← shrunk to minimal case
// Shrunk 18 time(s)Instead of a random large number pair, you get the exact boundary that breaks the code.
Configuration
// Per-test configuration
fc.assert(
fc.property(fc.string(), (s) => { ... }),
{
numRuns: 1000, // generate 1000 examples (default: 100)
seed: 42, // fixed seed for reproducibility
verbose: true, // show all generated examples
endOnFailure: false, // continue after first failure
}
);
// Global configuration
fc.configureGlobal({
numRuns: 50, // CI: fewer runs
});Reproducing Failures
When a test fails, fast-check prints a seed and path:
Property failed after 1 tests
{ seed: -123456789, path: "0", endOnFailure: true }
Counterexample: ["hello\uD800world"]Reproduce it exactly:
fc.assert(
fc.property(fc.string(), (s) => { ... }),
{ seed: -123456789, path: "0" }
);Jest/Vitest Integration
No special setup needed. Use fc.assert inside test() or it():
// Jest
describe('math properties', () => {
test('addition is commutative', () => {
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
expect(a + b).toBe(b + a);
})
);
});
});Real-World Example: Form Validation
const validFormInputs = fc.record({
username: fc.string({ minLength: 3, maxLength: 20 }).filter(/^[a-zA-Z0-9_]+$/.test.bind(/^[a-zA-Z0-9_]+$/)),
email: fc.emailAddress(),
password: fc.string({ minLength: 8, maxLength: 128 }),
age: fc.integer({ min: 13, max: 120 }),
});
const invalidEmails = fc.string().filter((s) => !s.includes('@'));
test('valid inputs always pass validation', () => {
fc.assert(
fc.property(validFormInputs, (form) => {
const result = validateForm(form);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
})
);
});
test('invalid email always fails validation', () => {
fc.assert(
fc.property(invalidEmails, (email) => {
const result = validateForm({ ...validBase, email });
expect(result.valid).toBe(false);
expect(result.errors).toContain('Invalid email');
})
);
});Pair with Functional Testing
fast-check finds bugs in your JavaScript/TypeScript logic by exploring input space. For continuous end-to-end monitoring of your production application — verifying real user flows work correctly 24/7 — HelpMeTest provides AI-powered functional test automation without requiring code.
Start free with HelpMeTest — 10 tests, no code required, monitoring every 5 minutes.