Header menu logo testify

DSL and Mental Model

Testify is not just a list of helpers. It is a small embedded testing DSL.

That matters because it explains why the API is split the way it is:

What “DSL” Means Here

In Testify, one test usually has this structure:

  1. a quoted expression or function
  2. a runner
  3. an expectation
  4. optional configuration, hints, or custom input generation

That gives Testify three syntax layers for the same underlying check.

One Idea, Three Syntax Layers

Assert

The most explicit named form:

Assert.should
    (AssertExpectation.equalTo 4)
    <@ 2 + 2 @>

The expectation-application DSL:

<@ 2 + 2 @> |>? AssertExpectation.equalTo 4

The shortest shorthand:

<@ 2 + 2 @> =? 4

All three mean the same thing.

Check

Named API:

Check.should(
    CheckExpectation.equalToReference,
    id,
    <@ List.rev >> List.rev @>)

Operator DSL:

<@ List.rev >> List.rev @> |=> id

Again, the operator is not a second engine. It is just a compact syntax layer over should.

Why Quotations Matter

Quotations are why Testify can do more than “expected true, got false.”

With <@ ... @>, Testify can retain the tested code shape and include it in:

That is why Testify asks for quotations even when the underlying semantic check is simple.

Runner vs Expectation

This is the central split in Testify’s design.

The runner answers:

The expectation answers:

So:

control execution,

while:

control meaning.

This split is what keeps the runner APIs small while still letting the library grow in expressive power.

result vs should

This is the most important behavioral split in the public API.

Use should when you want:

Use result when you want:

Operators are always fail-fast syntax over should, never over result.

Property Checks As “Generated Differential Testing”

Check is easiest to understand as a generated comparison pipeline:

  1. generate input values
  2. run the quoted tested function
  3. run the reference function or fixed oracle
  4. apply a CheckExpectation
  5. shrink failing cases
  6. render a structured failure report

That is why Check feels more powerful than a plain boolean property. It combines:

Where Hints And Config Fit

Hints and configuration are not side systems. They are second-order parts of the DSL.

So the full mental model is:

When To Stay Named And When To Use Operators

Prefer named APIs when:

Prefer operators when:

For the full symbolic reference, continue with Operator Cheat Sheet.

val id: x: 'T -> 'T
Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
val rev: list: 'T list -> 'T list

Type something to start searching.