F# Guide — Testing: Unit and Property-Based Testing
In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
F# testing uses standard .NET frameworks like xUnit and NUnit for unit tests plus FsCheck for property-based testing, taking advantage of pure functions and immutability for naturally testable code.
What You'll Learn
- xUnit unit testing in F#
- FsCheck property-based testing
- Test-driven development in F#
- Mocking and test doubles
- Integration testing patterns
Why It Matters
F#'s functional nature makes testing easier. Pure functions always return the same output for the same input, making tests deterministic.
Real-World Use
Financial systems use property-based testing for calculation invariants. Web services use integration tests for API validation.
flowchart LR
A["Testing"] --> B["xUnit"]
B --> C["FsCheck"]
C --> D["TDD"]
D --> E["Integration"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
xUnit Setup
// Add xUnit and FsUnit packages
// dotnet add package xunit
// dotnet add package FsUnit
module Tests
open Xunit
open FsUnit
[<Fact>]
let ``Adding 1 and 2 equals 3`` () =
let result = add 1 2
result |> should equal 3
Test Organization
module MathTests
open Xunit
let add x y = x + y
[<Fact>]
let ``add returns sum of two numbers`` () =
let result = add 3 5
Assert.Equal(8, result)
[<Theory>]
[<InlineData(1, 2, 3)>]
[<InlineData(-1, 1, 0)>]
[<InlineData(0, 0, 0)>]
let ``add with various inputs`` (a: int, b: int, expected: int) =
let result = add a b
Assert.Equal(expected, result)
FsCheck Property Tests
open FsCheck
open FsCheck.Xunit
[<Property>]
let ``Reversing a list twice returns the original`` (xs: int list) =
List.rev (List.rev xs) = xs
[<Property>]
let ``Sorting a list makes it non-decreasing`` (xs: int list) =
let sorted = List.sort xs
sorted = List.sort sorted
Custom Generators
open FsCheck
// Custom generator for positive numbers
let positiveInt =
Arb.generate<int>
|> Gen.filter (fun x -> x > 0)
type PositiveInt =
static member Int() =
Arb.fromGen positiveInt
[<Property(Arbitrary = [| typeof<PositiveInt> |])>]
let ``Division by positive number never fails`` (x: int) (y: int) =
y > 0 ==> lazy (try x / y |> ignore; true with _ -> false)
Test Fixtures
type DatabaseFixture() =
let connection = openConnection()
member this.Connection = connection
interface System.IDisposable with
member this.Dispose() = closeConnection connection
[<CollectionDefinition("Database")>]
type DatabaseCollection() =
interface ICollectionFixture<DatabaseFixture>
Mocking
// F# rarely needs mocks - pass functions instead
type IRepository =
abstract GetUser: int -> User option
let testRepository getUserFunc =
{ new IRepository with
member _.GetUser(id) = getUserFunc id }
let repo = testRepository (fun id -> Some { Name = "Test" })
Integration Tests
module IntegrationTests
open Xunit
open System.Net.Http
[<Fact>]
let ``API health check returns OK`` () = task {
use client = new HttpClient()
let! response = client.GetAsync("http://localhost:5000/health")
Assert.True(response.IsSuccessStatusCode)
}
Common Mistakes
1. Testing implementation details
Test behavior, not implementation. Pure functions make this natural.
2. Not using property tests
Property-based testing finds edge cases example-based tests miss. Use both.
3. Over-mocking
Functional F# rarely needs mocks. Pass functions as parameters instead.
4. Ignoring FsUnit
FsUnit provides F#-idiomatic assertions like should equal. More readable than Assert.Equal.
5. Slow test suites
Keep unit tests fast. Move slow integration tests to a separate test project.
Practice Questions
1. What is the difference between [
2. What is property-based testing? Testing that verifies invariants (properties) hold across many randomly generated inputs.
3. Why are F# functions naturally testable? Pure functions always return the same output for the same input, with no hidden state.
Challenge: Write property-based tests for a custom sorting function verifying idempotence, order, and element preservation.
FAQ
{{< faq question="What testing framework should I use?" >} xUnit is the most popular. NUnit also works well. Both support F# idiomatic testing patterns. {{< /faq >}}
{{< faq question="Do I need mocking frameworks?" >} Rarely in F#. Use function parameters for dependency injection instead of mocking interfaces. {{< /faq >}}
{{< faq question="How do I test async code?" >}
Use task { } or async { } in tests. xUnit supports async test methods returning Task.
{{< /faq >}}
{{< faq question="What is FsCheck?" >} A property-based testing framework inspired by Haskell's QuickCheck. It generates random test cases and shrinks failures. {{< /faq >}}
{{< faq question="How do I measure Code Coverage?" >}
Use Coverlet or dotCover with your test runner. Run dotnet test --collect:"XPlat Code Coverage".
{{< /faq >}}
Mini Project
Build a test suite for a simple calculator:
module Calculator
let add x y = x + y
let subtract x y = x - y
let multiply x y = x * y
let divide x y = if y = 0 then None else Some (x / y)
module Tests
open Xunit
open FsCheck.Xunit
[<Fact>]
let ``add adds two numbers`` () =
Assert.Equal(5, add 2 3)
[<Property>]
let ``add is commutative`` (a: int) (b: int) =
add a b = add b a
[<Property>]
let ``add is associative`` (a: int) (b: int) (c: int) =
add (add a b) c = add a (add b c)
[<Property>]
let ``divide by zero returns None`` (x: int) =
divide x 0 = None
What's Next
Now that you understand testing, explore .NET interop for calling C# code.
| Topic | Description | Link |
|---|---|---|
| F# .NET Interop | C# interop | {{< ref "27-net-interop" >}} |
| F# Fable | F# to JavaScript | {{< ref "28-fable" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro