Schema reference

A schema is one file describing one routine. It is read by Config::Abstraction, so YAML, JSON and Perl data files all work — YAML is what everything is tested with. Every other part of the toolkit consumes this same file.

KeyMeaning
moduleModule to load. Optional: guessed from the filename, so My-Widget.conf becomes My::Widget. The reserved value builtin means a Perl built-in.
functionThe function or method under test. Defaults to run.
newArguments for the constructor. Its presence makes the routine a method call on a constructed object.
inputNamed, positional or single-argument parameter schema.
outputWhat a successful return value looks like — or that the routine must die.
configSwitches controlling which extra probes run.
transformsNamed input-subset → output-subset relationships.
cases, yaml_casesA static corpus, inline or in a separate YAML file.
edge_cases, type_edge_cases, edge_case_arrayExtra values to inject, beyond the generated ones.
seed, iterationsReproducibility and how many random cases to run.
accessorDeclares the routine an accessor, so getter/setter tests are generated.

Naming the target

---
module: My::Widget
function: validate

If module is omitted the generator guesses from the file name: My-Widget.conf → My::Widget. If function is omitted it tests run, which is the right default for command modules.

The routine is a function call unless new is present. An empty new: is meaningful — it asks for Module->new() with no arguments and a method call thereafter:

module: My::Digest
function: hexdigest

new:

input

Named parameters

input:
  name:
    type: string
    optional: false
    min: 1
    max: 64
  age:
    type: integer
    optional: true
    min: 0
    max: 130

One unnamed parameter

input:
  type: string
  min: 1
  max: 1000

Positional parameters

When a routine takes a fixed argument order, say so with position. Positions start at 0. A parameter can carry both a name and a position; the name is used for diagnostics.

module: Math::Simple::MinMax
function: max

input:
  left:
    type: number
    position: 0
  right:
    type: number
    position: 1

output:
  type: number

The keyword undef in place of the whole input section means the routine takes no arguments:

input: undef

Types and constraints

KeyApplies toEffect
typeallOne of string, integer, float, number, boolean, arrayref, hashref, object, any, undef.
optionalallWhether the argument may be omitted. Generated calls respect it.
min, maxnumericInclusive bounds. Three boundary cases are generated per bound.
min, maxstringLength bounds, in characters.
min, maxarrayref, hashrefNumber of elements, or of keys.
lenstring, arraysExact length, treated as min == max.
matchesstringRegex the value must satisfy. Random strings are generated until they match.
nomatchstringRegex the value must not match.
memberof, enumstring, integer, numberAllowed values. Each one is tested, plus one value outside the list that should be rejected.
valueallAn exact expected or supplied value.
semanticstringA realistic data generator instead of random characters — see below.
canobjectMethods the stub object must provide. Without it, a mandatory object parameter makes the routine untestable and it is skipped.
isaobjectClass name; the harness emits use_ok and new_ok for it.

Boundary cases

For each declared constraint the harness adds three deterministic cases: just inside the range, exactly on the boundary, and just outside it — the last annotated _STATUS => 'DIES', because a well-written routine should reject it. Booleans get 0/1, true/false and off/on plus values that should be refused. An enum gets one case per allowed value plus one outside.

Semantic generators

Where a plain random string would never be valid, name a generator instead:

input:
  email:
    type: string
    semantic: email
    position: 0
ValueProduces
emailuser@domain.tld
urlHTTP and HTTPS URLs
uuidUUIDv4 identifiers
phone_usXXX-XXX-XXXX
phone_e164+XXXXXXXXXXXX
ipv4, ipv6Addresses
username, slugAlphanumeric names, and lowercase hyphenated slugs
hex_color#RRGGBB
iso_date, iso_datetimeYYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ
semvermajor.minor.patch
jwtBase64url-shaped tokens
json, base64Simple JSON objects; base64 strings
md5, sha256Hex hashes of the right length
unix_timestampEpoch seconds

A semantic generator can be combined with matches when you want realistic data that also satisfies a business rule:

input:
  corporate_email:
    type: string
    semantic: email
    matches: '@company\.com$'

output

output:
  type: number
  min: 0

The output schema is checked with Return::Set. It accepts the same type and constraint keys as an input parameter, plus two special forms:

FormMeaning
output: undefThe routine returns nothing; the harness asserts !defined.
_STATUS: DIESThe routine must croak for this input. Add it to an output spec, or to a single transform's output.
_STATUS: WARNSThe routine must warn instead of dying.
type: arrayThe routine returns a list. The harness captures it in list context and validates the arrayref (needs Return::Set 0.03+).
---
module: Scalar::Util
function: blessed

input:
  type: string

output: undef

config

KeyDefaultEffect
close_stdin1Stop the routine reading from standard input. Ignored on Windows.
test_nuls1Inject NUL bytes; the routine is expected to die.
test_undef1Try an undefined value.
test_empty1Try empty strings and empty containers.
test_non_ascii1Try strings containing non-ASCII characters.
timeout10Seconds before a call is abandoned. 0 disables the guard.
dedup1Remove duplicate generated cases.
properties0Property-based tests. Either 1, or a hashref with enable and trials.
test_security0Add security-shaped strings: SQL fragments, markup, control characters.

Turn a probe off when the routine legitimately accepts something the default probe assumes it rejects. A string-to-string function that happily accepts undef should set test_undef: no, or the generated test will assert a croak that never comes.

transforms

A transform names a relationship between a subset of the input and a subset of the output. Each one is exercised with its own inputs, and the result is checked against the transform's own output spec. With properties enabled, each transform also becomes a LectroTest property.

---
module: builtin
function: abs

config:
  test_undef: no
  test_empty: no
  test_nuls: no

input:
  number:
    type: number
    position: 0

output:
  type: number
  min: 0

transforms:
  positive:
    input:
      number:
        type: number
        position: 0
        min: 0
    output:
      type: number
      min: 0

  negative:
    input:
      number:
        type: number
        position: 0
        max: 0
    output:
      type: number
      min: 0

  error:
    input: undef
    output:
      _STATUS: DIES

For each transform the generator makes cases from its input spec, calls the routine, checks the return against the transform's output spec, and requires an exact match when the output names a value.

Properties on a transform

Add a properties list to an individual transform to assert an invariant beyond type and range. Several are built in:

NameAsserts
idempotentf(f(x)) == f(x)
non_negative, positive>= 0, > 0
non_emptyA string result is never empty
length_preservedOutput length equals input length
uppercase, lowercaseCase of a string result
trimmedNo leading or trailing whitespace
sorted_ascending, sorted_descendingOrder of an array result
unique_elementsAn array result contains no duplicates
preserves_keysA hash result has the same keys as the input

Anything the list does not cover can be written as Perl, with $result holding the return value and the input parameters available by name:

transforms:
  normalize:
    input:
      text:
        type: string
    output:
      type: string
    properties:
      - name: single_spaces
        description: No run of two or more spaces remains
        code: $result !~ /  /

      - name: reversible
        description: Length is unchanged
        code: length($result) == length($text)

cases and yaml_cases

A small static corpus maps an expected result to the arguments that produce it. This is the cheapest way to pin down behaviour you already know:

cases:
  ok:
    input: ping
    _STATUS: OK
  error:
    input: ""
    _STATUS: DIES

yaml_cases names a separate YAML file with the same shape, so a large corpus does not clutter the schema:

yaml_cases: t/conf/corpus/greet.yml

edge cases

Random generation will not stumble on the value that broke production. Say it instead. For named parameters, map parameter names to values:

edge_cases:
  name: [ "", "a" x 1024, "\x{263A}" ]
  age: [ -1, 0, 99999999 ]

For a routine with one unnamed parameter, use a list:

edge_case_array:
  - "<script>alert('xss')</script>"
  - "'; DROP TABLE users; --"
  - "\0null\0byte"
  - "emoji😊test"
  - ""
  - " "

During fuzzing there is a 40% chance that a case draws from the array instead of the random generator, so these values are always exercised.

type_edge_cases applies to every parameter of a given type, which is useful when a whole family of routines shares the same weakness:

type_edge_cases:
  string: [ "", " ", "\t", "\n", "\0", "long" x 1024, "\x{1F600}" ]
  number: [ 0, 1.0, -1.0, 1e308, -1e308, 1e-308, "NaN", "Infinity" ]
  integer: [ 0, 1, -1, 2147483647, -2147483648 ]

seed and iterations

seed: 42
iterations: 200

seed makes the generated harness call srand() with that value, so a failing run repeats exactly. iterations defaults to 30 random cases; the boundary and corpus cases are added on top of that, so the total test count is always higher.

accessor

accessor:
  property: ua
  type: getset

Declares the routine an accessor on property, with type one of getter, setter or getset. The generated tests then check the getter/setter contract — that a set is visible to a get, and that the getter with no argument does not change state.

A schema is a promise, not a description

The generated tests are black-box: they assert what the schema says the routine does. If a test fails, the question is whether the code or the schema is wrong — and the honest answer is sometimes the schema. Constraining a parameter to what the implementation happens to do today throws away the value of the test.

← Quick start Commands →