Header menu logo testify

Expectations and Composition

Expectations are the semantic heart of Testify.

The runner layer says:

The expectation layer says:

This is the core Testify design rule:

That lets the DSL stay stable while your domain vocabulary grows.

AssertExpectation

AssertExpectation<'T> is for one observed value.

Common categories:

The beginner path is usually:

equalBy vs equalByKey vs equalWith

These three are worth separating clearly.

equalBy

Project both the actual and expected full values, then compare the keys.

type Person = { Name: string; Age: int }

<@ { Name = "Tony"; Age = 48 } @>
|>? AssertExpectation.equalBy (fun person -> person.Age) { Name = "Anthony"; Age = 48 }

equalByKey

Project the actual full value, then compare the projected key to one explicit expected key.

<@ "Testify" @>
|>? AssertExpectation.equalByKey String.length 7

equalWith

Keep the full values and provide your own comparison relation.

<@ { Name = "Tony"; Age = 48 } @>
|>? AssertExpectation.equalWith (fun a b -> a.Age = b.Age) { Name = "Anthony"; Age = 48 }

CheckExpectation

CheckExpectation<'Args, 'Actual, 'Expected> is for tested code vs reference behavior.

Typical entry points:

The property-side pattern is similar:

Composition

Both expectation types compose the same way:

let bounded =
    AssertExpectation.greaterThan 0
    <&> AssertExpectation.lessThan 10

let relaxed =
    AssertExpectation.equalTo 0
    <|> bounded

And on the property side:

let relation =
    CheckExpectation.equalToReference
    <|> CheckExpectation.throwsSameExceptionType

And composition is chainable, not just binary:

let smallNatural =
    AssertExpectation.greaterThanOrEqualTo 0
    <&> AssertExpectation.lessThan 10
    <&> AssertExpectation.notEqualTo 7

let relaxed =
    AssertExpectation.equalTo "yes"
    <|> AssertExpectation.equalTo "y"
    <|> AssertExpectation.equalTo "true"

For longer sets of alternatives or requirements, sequence-based helpers such as any and all are often easier to read than a long operator chain.

Build Your Own Vocabulary

One of the best uses of expectations is to lift raw comparison logic into names that match your domain:

let sameAge =
    AssertExpectation.equalBy (fun person -> person.Age)

let successfulResult =
    AssertExpectation.isOk
    <&> AssertExpectation.satisfy "non-empty payload" (function Ok text -> text <> "" | Error _ -> false)

On the property side:

let sameVisibleBehavior =
    CheckExpectation.equalBy (fun user -> user.VisibleName)
    <|> CheckExpectation.throwsSameExceptionType

The expectation layer is where Testify becomes a language instead of only a helper library.

Rule Of Thumb

If the same idea appears in more than one test, give it a reusable expectation name instead of repeating inline logic.

type Person = { Name: string Age: int }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
module String from Microsoft.FSharp.Core
val length: str: string -> int
val bounded: obj
val relaxed: '_arg3
val relation: '_arg3
val smallNatural: '_arg3
val sameAge: 'a
val successfulResult: '_arg3
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val sameVisibleBehavior: '_arg3

Type something to start searching.