Skip to content

Fuzzing Patterns

Good fuzz targets assert a property — something true for every valid input — rather than a specific expected value. A few patterns cover most cases.

Round-trip (encode → decode)

Whatever you put in should come back out:

ts
import { expect, fuzz, FuzzSeed } from "as-test";

fuzz("encode/decode round-trips", (value: string): bool => {
  expect(decode(encode(value))).toBe(value);
  return true;
}).generate((seed: FuzzSeed, run: (value: string) => bool): void => {
  run(seed.string({ charset: "ascii", max: 64 }));
});

Invariant (a relationship that always holds)

Assert the relationship, not the exact output:

ts
fuzz("sorting preserves length and order", (xs: i32[]): bool => {
  const sorted = sort(xs);
  expect(sorted).toHaveLength(xs.length);
  for (let i = 1; i < sorted.length; i++) {
    expect(sorted[i] >= sorted[i - 1]).toBe(true);
  }
  return true;
}).generate((seed: FuzzSeed, run: (xs: i32[]) => bool): void => {
  run(seed.array<i32>((s: FuzzSeed): i32 => s.i32(), { min: 0, max: 32 }));
});

Bounded inputs

Constrain generators so inputs stay in the domain you actually support — that keeps failures meaningful instead of testing overflow you don't care about:

ts
fuzz("bounded addition is reversible", (a: i32, b: i32): bool => {
  expect(a + b - b).toBe(a);
  return true;
}).generate((seed: FuzzSeed, run: (a: i32, b: i32) => bool): void => {
  run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
});

Keep the generator narrow

The generator's only job is to build the input and call run(...). Don't mutate shared state or compute the expected answer inside it — that logic belongs in the property callback, where a failure can be tied back to its seed.

ts
// Good: generator only produces inputs
.generate((seed, run) => run(seed.i32(), seed.i32()));

When a property fails, as-test prints the seed and input and a repro command — see Failure Reproduction.