Skip to content
AgileLogics
7 min read

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; testAsync and testTask exist for asynchronous code.
  • Expect.equal actual expected message fails with a clear expected/actual diff.
  • In a project, create the console app with the Expecto template (dotnet new install Expecto.Template, then dotnet new expecto) and run it with dotnet run or dotnet 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.