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




















































