Testing with Expecto and xUnit
Prerequisites: Projects and NuGet packages
Pure functions — the bulk of a well-designed F# program — are the easiest code there is to test: same input, same output, no setup.
Expecto
Expecto is a popular F#-first test library: tests are ordinary values, grouped in lists, and run from a console app (or a script):
// illustrative: downloads Expecto on first run
#r "nuget: Expecto"
#r "nuget: Expecto.FsCheck"
open Expecto
let add a b = a + b
let tests =
testList "math" [
test "adds two numbers" {
Expect.equal (add 2 3) 5 "2 + 3 = 5"
}
test "adding zero changes nothing" {
Expect.equal (add 7 0) 7 "identity"
}
testProperty "addition is commutative" <| fun (a: int) (b: int) ->
add a b = add b a
test "a failing test" {
Expect.equal (add 2 2) 5 "2 + 2 should be 5?"
}
]
runTestsWithCLIArgs [] [||] tests |> printfn "exit code %d"
The output (timestamps and stack trace shortened):
[19:03:23 INF] EXPECTO? Running tests... <Expecto>
[19:03:23 ERR] math.a failing test failed in 00:00:00.0350000.
2 + 2 should be 5?.
expected: 5
actual: 4
at FSI_0002.tests@18-3.Invoke(Unit unitVar) in C:\demo\ex.fsx:line 18
...
[19:03:23 INF] EXPECTO! 4 tests run in 00:00:00.2426622 for math – 3 passed, 0 ignored, 1 failed, 0 errored. <Expecto>
exit code 1
test "name" { ... }is a computation expression;testAsyncandtestTaskexist for asynchronous code.Expect.equal actual expected messagefails with a clear expected/actual diff.- In a project, create the console app with the Expecto template
(
dotnet new install Expecto.Template, thendotnet new expecto) and run it withdotnet runordotnet test.
Property-based testing with FsCheck
testProperty above is powered by FsCheck: instead of a few
examples, you state a property that must hold for all inputs, and
FsCheck tries a hundred random ones — then shrinks any failing input to
the smallest example. Good properties include "reversing twice gives
the original", "serializing then deserializing round-trips", and
"sorting keeps the same items".
xUnit
xUnit is the most common .NET test framework; it's what the
dotnet new xunit -lang F# template uses, and F# works with it
nicely:
// illustrative: Tests.fs in a project created with dotnet new xunit -lang F#
module Tests
open Xunit
let discount total = if total >= 100m then total * 0.9m else total
[<Fact>]
let ``orders of 100 or more get 10 percent off`` () =
Assert.Equal(90m, discount 100m)
[<Fact>]
let ``small orders pay full price`` () =
Assert.Equal(50m, discount 50m)
[<Theory>]
[<InlineData(0)>]
[<InlineData(99)>]
let ``totals below 100 are unchanged`` (total: int) =
Assert.Equal(decimal total, discount (decimal total))
$ dotnet test
...
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 169 ms - Shop.Tests.dll (net10.0)
Double-backtick names (``like this``) let test names be readable
sentences. Unquote (test <@ discount 100m = 90m @>) and
FsUnit (discount 100m |> should equal 90m) add more F#-flavored
assertions on top of xUnit or NUnit.
What to test
- Domain logic — pure functions, validation, state transitions. Most of your tests belong here, and they run in milliseconds.
- Boundaries — parsing, serialization, and database access, with a few integration tests.
- Web handlers — ASP.NET Core's
WebApplicationFactory(or Giraffe's test helpers) runs the app in memory for end-to-end tests.
Keeping I/O at the edges (see Designing with Types) is what makes this split possible.