Header menu logo testify

Power Showcase

This page is the short answer to:

“Why would I reach for Testify instead of just writing plain asserts?”

1. Compare Domain Objects By Meaning, Not Raw Equality

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

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

This is stronger than “assert equal” because the test says what counts as equal in this domain.

2. Compare Against A Derived Key

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

This is ideal when the important expectation is not the whole value, but a projected fact about it.

3. Keep Full Values, But Supply Your Own Relation

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

This is useful when projection is too weak and raw equality is too strong.

4. Reference-Based Property Check

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

This is one of Testify’s signature moves:

5. Shape The Input Space

let pairArb =
    Arbitraries.tuple2
        (Arbitraries.from<int>)
        (Arbitraries.from<int>)

Check.should(
    CheckExpectation.equalToReference,
    (fun (a, b) -> a + b),
    <@ fun (a, b) -> a + b @>,
    arbitrary = pairArb)

This is where property testing stops being “whatever the default generator gives me” and starts becoming intentional.

6. Dependent Property With Custom Quantification

Check.shouldBy(
    (fun verify ->
        FsCheck.Prop.forAll Arbitraries.from<int> (fun n ->
            let length = abs n
            let arb =
                Arbitraries.fromGen (
                    FsCheck.Gen.listOfLength length FsCheck.Arb.generate<int>
                )

            FsCheck.Prop.forAll arb (fun xs ->
                verify (length, xs)))),
    CheckExpectation.isTrue,
    (fun _ -> true),
    <@ fun (expectedLength, xs) -> List.length xs = expectedLength @>)

This is the “plain arbitrary is not enough” escape hatch, and it keeps the property readable instead of forcing specialized Check2 / Check3 style overloads.

7. Inspect A Failure Instead Of Throwing

let result =
    Check.result(
        CheckExpectation.equalToReference,
        (fun x -> x + 1),
        <@ fun x -> x + 2 @>)

let rendered = Check.toDisplayString result
let report = Check.toFailureReport result

This is why the result / should split matters:

8. Turn Failures Into Guidance Instead Of Mere Mismatches

Testify.configure (
    TestifyConfig.defaults
    |> TestifyConfig.withHintPacks BuiltInHintPacks.beginner
)

let result =
    Assert.result
        (AssertExpectation.equalToWithDiff Diff.defaultOptions "MiniLib")
        <@ "MiniLib " @>

With hint packs enabled, the rendered failure can combine:

That is a much more teacher-friendly and debugger-friendly output shape than a plain equality failure.

9. Replay A Counterexample

match result with
| Failed failure ->
    match failure.TryGetReplayConfig() with
    | Some replayConfig ->
        let replayed =
            Check.result(
                CheckExpectation.equalToReference,
                (fun x -> x + 1),
                <@ fun x -> x + 2 @>,
                config = replayConfig)

        printfn "%s" (Check.toDisplayString replayed)
    | None ->
        ()
| _ ->
    ()

That is a much nicer debugging loop than trying to rediscover the failing input manually.

The Point

The power of Testify is not one single feature.

It is the combination of:

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
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
val id: x: 'T -> 'T
val pairArb: 'a
val abs: value: 'T -> 'T (requires member Abs)
val length: list: 'T list -> int
val result: 'a
val rendered: 'a
val report: 'a
val failure: 'a
union case Option.Some: Value: 'T -> Option<'T>
val replayConfig: 'a
val replayed: 'a
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
union case Option.None: Option<'T>

Type something to start searching.