A quick reference for using repeat-test. See the tutorials for more details:
import { arb, repeatTest } from "@skybrian/repeat-test";
import { assert } from "@std/assert";
// Basic property test
repeatTest(arb.int(0, 100), (n, console) => {
console.log("testing:", n); // Only prints on failure
assert(n >= 0);
});
// Multiple inputs using arb.object
const input = arb.object({ s: arb.string(), n: arb.int(0, 10) });
repeatTest(input, ({ s, n }, console) => {
// Test with string s and int n
});
// With explicit examples
repeatTest(["hello", "world", arb.string()], (s) => {
// Runs with "hello", "world", then random strings
});Arbitraries generate random values for testing.
| Function | Description |
|---|---|
arb.int(min, max) |
Integer in range [min, max] |
arb.int32() |
Signed 32-bit integer |
arb.safeInt() |
Any safe integer |
arb.boolean() |
true or false |
arb.biased(p) |
Boolean with probability p of being true |
| Function | Description |
|---|---|
arb.string(opts?) |
Any JS string (may have unpaired surrogates) |
arb.wellFormedString(opts?) |
Well-formed Unicode string |
arb.asciiChar(regexp?) |
Single ASCII character |
arb.asciiLetter() |
a-z or A-Z |
arb.asciiDigit() |
0-9 |
arb.asciiSymbol() |
ASCII punctuation/symbols |
arb.asciiWhitespace() |
ASCII whitespace |
arb.char16() |
Any single 16-bit code unit |
arb.unicodeChar() |
Single Unicode code point (1-2 chars) |
| Function | Description |
|---|---|
arb.array(item, opts?) |
Array of items |
arb.uniqueArray(item, opts?) |
Array with unique items (item must be Domain) |
arb.object(shape) |
Object with given shape |
arb.union(...cases) |
One of several object shapes |
arb.table(row, opts?) |
Array of objects with optional unique keys |
ArrayOpts: { length?: number | { min?: number, max?: number } }
| Function | Description |
|---|---|
arb.of(...values) |
One of the given constant values |
arb.oneOf(...arbs) |
Value from one of the given Arbitraries |
arb.from(buildFn) |
Custom Arbitrary from build function |
arb.alias(init) |
Lazy/recursive Arbitrary |
Domains are Arbitraries that can also parse/validate values. Use when you need round-trip validation.
| Function | Description |
|---|---|
dom.int(min, max) |
Integer in range |
dom.int32() |
Signed 32-bit integer |
dom.boolean() |
Boolean |
dom.string(opts?) |
Any JS string |
dom.wellFormedString(opts?) |
Well-formed Unicode string |
dom.asciiChar(regexp?) |
Single ASCII character |
dom.asciiLetter() |
a-z or A-Z |
dom.char16() |
Single 16-bit code unit |
dom.array(item, opts?) |
Array of items |
dom.uniqueArray(item, opts?) |
Array with unique items |
dom.object(shape, opts?) |
Object with shape |
dom.taggedUnion(tagProp, cases) |
Discriminated union |
dom.table(row, opts?) |
Array of objects |
dom.of(...values) |
One of given values |
dom.firstOf(...doms) |
First matching domain |
dom.alias(init) |
Lazy/recursive Domain |
// Using arb.from with a build function
const point = arb.from((pick) => {
const x = pick(arb.int(0, 100));
const y = pick(arb.int(0, 100));
return { x, y };
});
// Using arb.object (simpler for plain objects)
const point2 = arb.object({
x: arb.int(0, 100),
y: arb.int(0, 100),
});
// Filtering
const evenInt = arb.int(0, 100).filter((n) => n % 2 === 0);
// Mapping
const doubled = arb.int(0, 50).map((n) => n * 2);
// Chaining (dependent generation)
const pair = arb.int(1, 10).chain((n) =>
arb.array(arb.int(0, 100), { length: n })
);import { arb, type Arbitrary } from "@skybrian/repeat-test";
type Tree = { value: number; children: Tree[] };
const tree: Arbitrary<Tree> = arb.alias(() =>
arb.object({
value: arb.int(0, 100),
children: arb.array(tree, { length: { max: 3 } }),
})
);Use console.sometimes(key, condition) to ensure arbitraries generate
sufficient variety. The test fails if the condition is never true or never false
across all repetitions.
repeatTest(arb.int(-100, 100), (n, console) => {
console.sometimes("positive", n > 0);
console.sometimes("negative", n < 0);
console.sometimes("zero", n === 0);
});Use console.checkOdds(key, expectedProb, condition) to verify a condition
occurs with an expected probability:
repeatTest(arb.int(0, 99), (n, console) => {
// Verify divisibility by 10 occurs ~10% of the time
console.checkOdds("divisible by 10", 0.1, n % 10 === 0);
});
// Also works with small sets using exact comparison
repeatTest(arb.boolean(), (val, console) => {
console.checkOdds("true", 0.5, val); // exactly 1/2 of {true, false}
});For small sets where all values are enumerated, it compares the exact ratio. For larger sampled sets, it performs a statistical test.
Property tests should verify that generated values satisfy expected constraints:
repeatTest(arb.array(arb.int(0, 100)), (arr, console) => {
const sorted = [...arr].sort((a, b) => a - b);
assert(sorted.length === arr.length);
for (let i = 1; i < sorted.length; i++) {
assert(sorted[i - 1] <= sorted[i]);
}
});Put all checks for the same arbitrary in a single repeatTest call for better
performance:
// Good: one repeatTest with multiple assertions
repeatTest(myArbitrary, (val, console) => {
console.sometimes("case A", checkA(val));
console.sometimes("case B", checkB(val));
assert(invariant(val));
});
// Less efficient: separate repeatTest calls
repeatTest(myArbitrary, (val) => assert(invariant(val)));
repeatTest(myArbitrary, (val, t) => t.sometimes("case A", checkA(val)));Every Arbitrary has a default value. For example, the default for arb.string()
is the empty string, and the default for arb.int(0, 100) is 0. When
repeatTest runs an Arbitrary, it tries the default value first as a smoke test
before generating random values.
When writing a custom Arbitrary with arb.from(), think about what the default
value should be. The default is determined by each nested pick() call
returning its own default. For IntRequest, the default is always the minimum
of the range. For arb.boolean(), the default is false. For arb.oneOf(),
the default comes from the first case.
See defaults.md for a complete table of default values.
Here are some tips to make property tests run faster:
For strings and arrays, test shorter values more often. Built-in arbitraries
already do this. When writing custom arbitraries with arb.from(), consider
picking between adding an item or ending:
// This shrinks better than generating a length then filling
const shortList = arb.from((pick) => {
const items: number[] = [];
while (pick(arb.boolean())) { // 50% chance to continue
items.push(pick(arb.int(0, 100)));
}
return items;
});For further performance optimization, the number of pick calls can sometimes be
reduced by using a single pick to make multiple decisions. This is an advanced
technique that uses IntRequest directly instead of higher-level Arbitraries.
IntRequest is the low-level primitive that all Arbitraries are built on. It
picks a non-negative integer in a range, and its default value is always the
minimum of the range. By carefully choosing ranges, you can encode multiple
decisions in one pick:
import { arb, IntRequest } from "@skybrian/repeat-test";
// IntRequest(0, 201) gives 202 values:
// - 0-100: stop (~50% chance; 0 is default, so empty list is default)
// - 101-201: continue with value 0-100 (~50% chance)
const stopOrValue = new IntRequest(0, 201);
const shortListOptimized = arb.from((pick) => {
const items: number[] = [];
while (true) {
const choice = pick(stopOrValue);
if (choice <= 100) {
break; // stop
}
items.push(choice - 101); // map 101-201 to 0-100
}
return items;
});This encodes both the "continue or stop" decision and the item value in a single pick call, reducing overhead. The key insight is that the default (0) must correspond to termination, otherwise the default value generation will loop forever.
Write property tests for custom arbitraries using console.sometimes() to
verify they generate the expected variety:
repeatTest(myCustomArbitrary, (val, console) => {
console.sometimes("has property X", hasPropertyX(val));
console.sometimes("has property Y", hasPropertyY(val));
// Verify invariants always hold
assert(isValid(val));
});Also consider verifying the default value is what you expect:
import { assertEquals } from "@std/assert";
import { arb, generateDefault } from "@skybrian/repeat-test";
const myArbitrary = arb.from((pick) => {
// ... build logic ...
});
// Test the default value directly
assertEquals(generateDefault(myArbitrary).val, expectedDefault);Deep-freeze objects for use as test examples:
import { frozen, repeatTest } from "@skybrian/repeat-test";
repeatTest([frozen({ a: 1 }), frozen({ nested: { b: 2 } })], (obj) => {
// obj is deeply frozen
});Use the REPS environment variable to control how many repetitions each test
runs. The value is specified as either a percentage or a multiplier relative to
the baseline rep count (default 1000, or opts.reps if specified).
For faster iteration during development, use a low percentage:
REPS=1% deno test --allow-env # ~10 reps per test (1% of 1000)
REPS=5% deno test --allow-env # ~50 reps per test
REPS=0.5x deno test --allow-env # ~500 reps per testWhen the multiplier is less than 1, sometimes() validation is skipped since
low rep counts may not satisfy coverage requirements.
Note: The rep count is always at least 1, so REPS=0% runs 1 rep per test.
For more thorough testing, use a higher multiplier:
REPS=5x deno test --allow-env # 5000 reps per test
REPS=500% deno test --allow-env # 5000 reps per test (same as 5x)This simply runs more repetitions, which increases the chance of finding bugs that only occur with specific combinations of random inputs.
REPS=100% or REPS=1x is equivalent to not setting REPS at all.
Note: REPS is ignored when repeatTest is called with the only option,
since that mode is intended for reproducing a specific failing rep.