Expecto is an open-source .NET test framework that fits naturally with F# values and functions. It replaces hand-written runners and repeated assertions with named tests, grouped test lists, and a console application that runs them. section 14.2.2 section 14.2.3
A test is created with `test`, giving it a readable string name and a body. Combine tests with `testList`, then pass the resulting value to `runTestsWithCLIArgs`. Failure output identifies the fully qualified test name, shows expected and actual values, and reports a run summary. section 14.2.3
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 tests a music-playing function. Each assertion supplies an expected value and a failure message; the list is then run from the application entry point. section 14.2.3
Tests and test groups share the `Test` abstraction, so groups can be nested and composed as ordinary values. This avoids attribute-based discovery: build a suite in code, run a selected group in isolation, or generate tests from environment settings and input data. section 14.2.4 section 14.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 suite conditionally includes a CI-only test and creates addition tests from lines in a file, using ordinary F# control flow rather than specialized test-framework features. section 14.2.4
Use Expecto when test orchestration needs to remain explicit and programmable:
- Nest database, business-logic, and complete-suite test lists into a hierarchy. section 14.2.4
- Choose whether tests run from values such as environment variables or file data. section 14.2.4
- Compare two functions with basic performance checks through `Performance.isFasterThan`; use Benchmark.NET for serious timing and memory benchmarks. section 14.5.1
Expecto contrasts with NUnit and XUnit styles that commonly rely on attributed functions, test discovery, and an external runner. Those frameworks work with F#, but explicit test values provide more flexibility when suites must be assembled or controlled beyond a fixed discovery model. section 14.6 section 14.4
