liveBook · Manning

Test

As explained in F# in Action: A Revised Edition of Get Programming with F#

Test is Expecto's compositional test value, representing either an individual test or a collection of nested tests.

Automated testing is code that sets up known input or state, calls production code, and checks for the expected result. Small, reliable tests can run repeatedly during builds or check-ins, increasing confidence when existing code changes. Favor the areas where failure is risky or logic is complex; inconsistent, brittle, or slow tests lose their value. section 14.1

F# types reduce—not eliminate—the need for tests. Put validation and invariants at boundaries: a function that accepts a ValidatedEmail can focus on sending mail, while validation is tested once where the validated value is created. Discriminated unions and exhaustive pattern matching similarly let the compiler protect known cases. section 14.1.1

A basic test is an F# function following Arrange, Act, Assert: prepare test data, call the function under test, then compare actual and expected output. Keep tests simple. A complicated test often indicates that it exercises too much at once or that the production code is difficult to test. section 14.2.1

Compose tests with Expecto

Expecto represents both an individual test and a named group as Test values. Use test for a test body, testList for a group, and runTestsWithCLIArgs to execute the resulting value. Names remain ordinary strings, while nested lists produce readable qualified test names in failure output. section 14.2.3 section 14.2.4

Listing 14.3 Unit tests written with Expecto
open Expecto
 
let musicTests = testList "Music tests" [                             #1
    test "My guitar gently weeps" {                                   #2
        let input = Guitar
        let output = play input
        Expect.equal output "Wah wah" "Guitar sound is incorrect"     #3
    }
 
    test "Drums go bang" {                                            #2
        let input = Drums
        let output = play input
        Expect.equal output "Ba dum tss" "Drums are wrong"            #3
    }
]
 
[<EntryPoint>]
let main args =
    runTestsWithCLIArgs [] args musicTests                            #4

This small suite shows two test values grouped under one named testList and passed directly to the command-line runner. section 14.2.3

Because tests are ordinary values, compose groups into larger hierarchies, select tests with ordinary F# conditions, or generate tests from input-file rows. This avoids attribute-based discovery and supports needs such as CI-only tests without special framework constructs. section 14.2.4

Listing 14.4 Programmatically creating tests in Expecto
let add a b = a + b                                               #1
 
let myTests = testList "My Tests" [
    if System.Environment.GetEnvironmentVariable
 "CI_SERVER" <> null then                                       #2
        test "CI Only Test" { Expect.equal (add 1 2)
 3 "Something is very wrong!" }
 
    for line in System.IO.File.ReadAllLines "testData.txt" do     #3
        test $"Addition test: {line}" {                           #4
            match line.Split ',' with
            | [| inputA; inputB; expected |] ->
                let actual = add (int inputA) (int inputB)
                Expect.equal actual (int expected) "Calculator is broken"
            | _ ->
                failwith $"Invalid test data format: {line}"
        }
]
#1 Our production code, a simple addition function
#2 Programmatically decides whether to yield back a test based on the existence of an environment variable
#3 Reads data from a file using basic .NET code
#4 For each line in the file, yields back a new test whose name is based on the line in the file

The listing builds a suite conditionally from an environment variable and repeatedly from lines in a test-data file. section 14.2.4

For code that stays practical to test:

  • Test smaller functions or small function groups from the bottom up, where algorithmic logic often lives. section 14.3.3
  • Begin without a framework if useful, then move to a runner that integrates with development tools. section 14.2
  • Use tests alongside types rather than assuming compilation alone proves behavior. section 14.1.1

How F# in Action: A Revised Edition of Get Programming with F# covers it

How deep it goes
Works through AAA function tests and Expecto composition, while deferring comprehensive unit-testing practice.
Examples
Uses instrument sounds, validated email, nested suites, and addition cases read from a text file.
What it assumes
Starts from basic automated-test functions and introduces framework use without requiring prior test-framework knowledge.
What you can do after
Apply it via Expecto to compose, run, conditionally select, and data-generate named F# tests.
How it connects
Connects testing with F# types, discriminated unions, pattern matching, test runners, and continuous integration.

Also explained in

Compare how 53 books explain test →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of F# in Action: A Revised Edition of Get Programming with F#. The section links point to the parts of the book it draws on.