A property-based testing library for TypeScript, inspired by the Haskell Hedgehog library. Hedgehog automatically generates test cases and provides integrated shrinking to find minimal failing examples.
- Property-based testing: Define properties that should hold for all inputs, and Hedgehog generates test cases automatically
- Integrated shrinking: When tests fail, Hedgehog automatically finds the smallest input that reproduces the failure
- High-performance random generation: Adaptive seed implementation automatically optimizes between WASM and JavaScript based on workload
- Transparent batch optimization: Bulk operations automatically use optimized batching for 18-128x performance improvements
- Splittable random generation: Uses SplitMix64 for independent, reproducible random streams
- Type-safe generators: Compositional generators with full TypeScript type safety
This library features a unique AdaptiveSeed implementation that is used by default and transparently chooses the optimal random number generation strategy:
- WASM implementation: Rust-compiled WebAssembly for maximum computational speed (2.89x faster construction, 1.92x faster boolean generation)
- BigInt implementation: Pure TypeScript for complex workflows (2.70x faster for chained operations)
- Automatic batching: Bulk operations automatically use WASM batching when beneficial (18-128x speedup for large operations)
- Silent fallback: Gracefully falls back to BigInt if WASM is unavailable
The default Seed export is AdaptiveSeed - it automatically selects the best approach based on operation patterns, providing optimal performance without user intervention.
npm install hedgehogimport { forAll, Gen, int, string } from 'hedgehog';
// Define a property: reversing a list twice gives the original list
const reverseTwiceProperty = forAll(
Gen.array(int(1, 100)),
(list) => {
const reversed = list.reverse().reverse();
return JSON.stringify(reversed) === JSON.stringify(list);
}
);
// Test with complex data structures
const userProperty = forAll(
Gen.object({
id: int(1, 1000),
name: Gen.optional(string()), // string | undefined
email: Gen.nullable(string()), // string | null
status: Gen.union( // 'active' | 'inactive' | 'pending'
Gen.constant('active'),
Gen.constant('inactive'),
Gen.constant('pending')
)
}),
(user) => {
// Property: user objects have valid structure
return typeof user.id === 'number' &&
user.id > 0 &&
['active', 'inactive', 'pending'].includes(user.status);
}
);
// Test the properties
console.log('Reverse property:', reverseTwiceProperty.check().ok);
console.log('User property:', userProperty.check().ok);Properties are statements that should be true for all valid inputs:
import { forAll, Gen, int, string } from 'hedgehog';
// Property: string length is preserved under concatenation
const concatenationProperty = forAll(
Gen.tuple(string(), string()),
([a, b]) => {
const result = a + b;
return result.length === a.length + b.length;
}
);
// Property: addition is commutative
const commutativeProperty = forAll(
Gen.tuple(int(0, 1000), int(0, 1000)),
([a, b]) => a + b === b + a
);Generators produce random test data of specific types:
import { Gen } from 'hedgehog';
// Basic generators
const boolGen = Gen.bool();
const numberGen = Gen.int(1, 100);
const stringGen = Gen.string();
// Composite generators
const arrayGen = Gen.array(numberGen);
const objectGen = Gen.object({
id: numberGen,
name: stringGen,
active: boolGen
});
// Transformed generators
const evenNumberGen = numberGen
.filter(n => n % 2 === 0)
.map(n => n * 2);Handle nullable, optional, and union types elegantly:
// Optional and nullable generators
const optionalName = Gen.optional(stringGen); // string | undefined
const nullableId = Gen.nullable(numberGen); // number | null
// Union types
const statusGen = Gen.union(
Gen.constant('pending'),
Gen.constant('success'),
Gen.constant('error')
); // 'pending' | 'success' | 'error'
// Discriminated unions for complex types
interface SuccessResult {
type: 'success';
data: string;
}
interface ErrorResult {
type: 'error';
message: string;
}
const resultGen = Gen.discriminatedUnion('type', {
success: Gen.object({
type: Gen.constant('success' as const),
data: stringGen
}),
error: Gen.object({
type: Gen.constant('error' as const),
message: stringGen
})
}); // SuccessResult | ErrorResult
// Weighted unions for probability control
const biasedBoolGen = Gen.weightedUnion([
[9, Gen.constant(true)], // 90% true
[1, Gen.constant(false)] // 10% false
]);The Seed class provides deterministic random generation. By default, this is the AdaptiveSeed implementation which automatically optimizes performance:
import { Seed, Gen, Size } from 'hedgehog';
// This is AdaptiveSeed - automatically optimized
const seed = Seed.fromNumber(42);
const size = Size.of(10);
const gen = Gen.int(1, 100);
// Generate the same value every time with the same seed
const tree1 = gen.generate(size, seed);
const tree2 = gen.generate(size, seed);
console.log(tree1.value === tree2.value); // true
// Split seeds for independent generation
const [leftSeed, rightSeed] = seed.split();
const leftValue = gen.generate(size, leftSeed).value;
const rightValue = gen.generate(size, rightSeed).value;
// leftValue and rightValue are independent
// Check what implementation is being used
console.log(seed.getImplementation()); // 'wasm' | 'bigint' | 'bigint-fallback'The default Seed automatically chooses the optimal implementation:
import { Seed } from 'hedgehog';
const seed = Seed.fromNumber(42);
// Single operations: automatically uses WASM for speed
const [bool, newSeed] = seed.nextBool(); // 1.92x faster with WASM
// Bulk operations: automatically batches with WASM
const result = seed.nextBools(1000); // 18.37x faster with batching
// Complex workflows: automatically uses BigInt for efficiency
// (Multiple chained operations favor BigInt due to lower overhead)Check which implementation is being used:
console.log(seed.getImplementation()); // 'wasm', 'bigint', or 'bigint-fallback'
const perfInfo = seed.getPerformanceInfo();
console.log(perfInfo.batchingAvailable); // true if WASM batching available
console.log(perfInfo.recommendedForBulkOps); // true if optimal for bulk operationsFor advanced use cases, you can choose specific implementations:
import { Seed as BigIntSeed } from 'hedgehog/seed/bigint';
import { Seed as WasmSeed } from 'hedgehog/seed/wasm';
import { AdaptiveSeed } from 'hedgehog/seed/adaptive';
// Explicit BigInt usage (pure JavaScript, works everywhere)
const bigintSeed = BigIntSeed.fromNumber(42);
const [bool1, newSeed1] = bigintSeed.nextBool(); // Individual operations
// Explicit WASM usage (fastest for computational workloads)
const wasmSeed = WasmSeed.fromNumber(42);
const [bool2, newSeed2] = wasmSeed.nextBool(); // Individual: 1.92x faster
const bulkBools = wasmSeed.nextBools(1000); // Batched: 18.37x faster
// Force BigInt even in AdaptiveSeed
const forcedBigInt = AdaptiveSeed.fromNumberBigInt(42);All implementations support both individual operations and bulk operations, with WASM providing significant performance advantages for both single calls and batched operations.
AdaptiveSeed is the default Seed implementation that provides transparent optimization:
- Tries WASM first for computational advantages (2.89x faster construction, 1.92x faster booleans)
- Silent fallback to BigInt if WASM is unavailable (ensures your code always works)
- Never fails due to environment issues - always provides a working implementation
- Individual calls for operations ≤ 10 (uses fastest available implementation)
- WASM batching for operations > 10 when available (18-128x speedup)
- Intelligent thresholds based on comprehensive benchmarking data
Use the default (AdaptiveSeed) - Recommended for 99% of use cases:
import { Seed } from 'hedgehog'; // This is AdaptiveSeed
const seed = Seed.fromNumber(42); // Automatically optimizedUse explicit BigInt when you need:
- Pure JavaScript (no WASM dependencies)
- Complex chained operations (2.70x faster for workflows)
- Guaranteed memory efficiency
import { Seed as BigIntSeed } from 'hedgehog/seed/bigint';
const seed = BigIntSeed.fromNumber(42); // Pure JavaScriptUse explicit WASM when you need:
- Maximum computational performance
- Bulk operations with guaranteed batching
- Known WASM-available environment
import { Seed as WasmSeed } from 'hedgehog/seed/wasm';
const seed = WasmSeed.fromNumber(42); // Pure WASM, no fallbackCreate domain-specific generators:
// Email generator
const emailGen = Gen.tuple(
Gen.stringOfLength(Gen.int(3, 10)),
Gen.constant('@'),
Gen.stringOfLength(Gen.int(3, 8)),
Gen.constant('.com')
).map(([name, at, domain, tld]) => name + at + domain + tld);
// Tree structure generator
interface TreeNode {
value: number;
children: TreeNode[];
}
const treeGen: Gen<TreeNode> = Gen.sized(size => {
if (size.value <= 1) {
return Gen.object({
value: Gen.int(1, 100),
children: Gen.constant([])
});
}
return Gen.object({
value: Gen.int(1, 100),
children: Gen.array(treeGen.scale(s => s.scale(0.5)))
});
});For performance-critical bulk generation:
const seed = Seed.fromNumber(42);
// Generate many booleans efficiently (uses automatic batching)
const boolResult = seed.nextBools(10000); // 18-128x faster than individual calls
console.log(boolResult.values.length); // 10000
console.log(boolResult.finalSeed); // Updated seed for further generation
// Generate many bounded values
const boundedResult = seed.nextBoundedBulk(5000, 100);
console.log(boundedResult.values.every(v => v >= 0 && v < 100)); // trueConfigure test execution:
import { Config } from 'hedgehog';
const config = Config.default()
.withTestLimit(1000) // Run 1000 test cases
.withShrinkLimit(100) // Try up to 100 shrinking attempts
.withSeed(42); // Use specific seed for reproducibility
const result = property.check(config);Based on comprehensive benchmarking on Apple M3 Pro:
- Construction: 2.89x faster than BigInt
- Boolean generation: 1.92x faster than BigInt
- Single operations: Optimal for isolated calls
- Complex workflows: 2.70x faster than WASM
- Memory efficiency: 3x less allocation pressure
- Chained operations: Lower object creation overhead
- Small batches (≤10): Uses individual calls
- Large batches (>10): Automatic WASM batching
- Optimal batch size: 1000 operations (18.37x speedup)
- Maximum observed speedup: 128.62x for very large batches
The AdaptiveSeed automatically switches between implementations based on these characteristics, ensuring optimal performance for any workload.
If using WASM features and building from source:
- Rust: 1.88.0 or newer
- wasm-pack: 0.13.1 or newer
- Node.js: 18.0.0 or newer
Build WASM module:
npm run build:wasmThe library gracefully falls back to pure JavaScript if WASM is unavailable.
Run the test suite:
npm test # Run tests (fast)
npm run test:watch # Watch modePerformance analysis:
npm run bench # Run performance benchmarksType checking and linting:
npm run typecheck # Type check
npm run lint # Lint code
npm run lint:fix # Fix lint issuesContributions welcome! This library follows these principles:
- Performance: Automatic optimization without user complexity
- Type safety: Full TypeScript support with precise types
- Composability: Generators should compose naturally
- Determinism: Reproducible test runs with seed control
- Simplicity: Clear, obvious APIs that just work
BSD-3-Clause
- Performance Analysis - Detailed benchmarking results
- WASM Bindings - Technical details of WASM integration
- Hedgehog (Haskell) - Original inspiration
- Property-based testing - Background concepts