fsharp-testing

F# testing patterns with xUnit, FsUnit, Unquote, FsCheck property-based testing, integration tests, and test organization best practices.

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill fsharp-testing
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

F# Testing Patterns

Comprehensive testing patterns for F# applications using xUnit, FsUnit, Unquote, FsCheck, and modern .NET testing practices.

When to Activate

  • Writing new tests for F# code
  • Reviewing test quality and coverage
  • Setting up test infrastructure for F# projects
  • Debugging flaky or slow tests

Test Framework Stack

Tool

Purpose

xUnit

Test framework (standard .NET ecosystem choice)

FsUnit.xUnit

F#-friendly assertion syntax for xUnit

Unquote

Assertion library using F# quotations for clear failure messages

FsCheck.xUnit

Property-based testing integrated with xUnit

NSubstitute

Mocking .NET dependencies

Testcontainers

Real infrastructure in integration tests

WebApplicationFactory

ASP.NET Core integration tests

Unit Tests with xUnit + FsUnit

Basic Test Structure

module OrderServiceTests

open Xunit

open FsUnit.Xunit

[<Fact>]

let ``create sets status to Pending`` () =

    let order = Order.create "cust-1" [ validItem ]

    order.Status |> should equal Pending

[<Fact>]

let ``confirm changes status to Confirmed`` () =

    let order = Order.create "cust-1" [ validItem ]

    let confirmed = Order.confirm order

    confirmed.Status |> should be (ofCase <@ Confirmed @>)

Assertions with Unquote

Unquote uses F# quotations so failure messages show the full expression that failed, not just "expected X got Y".

module OrderValidationTests

open Xunit

open Swensen.Unquote

[<Fact>]

let ``PlaceOrder returns success when request is valid`` () =

    let request = { CustomerId = "cust-123"; Items = [ validItem ] }

    let result = OrderService.placeOrder request

    test <@ Result.isOk result @>

[<Fact>]

let ``order total sums item prices`` () =

    let items = [ { Sku = "A"; Quantity = 2; Price = 10m }

                  { Sku = "B"; Quantity = 1; Price = 5m } ]

    let total = Order.calculateTotal items

    test <@ total = 25m @>

[<Fact>]

let ``validated email rejects empty input`` () =

    let result = ValidatedEmail.create ""

    test <@ Result.isError result @>

Async Tests

[<Fact>]

let ``PlaceOrder returns success when request is valid`` () = task {

    let deps = createTestDeps ()

    let request = { CustomerId = "cust-123"; Items = [ validItem ] }

    let! result = OrderService.placeOrder deps request

    test <@ Result.isOk result @>

}

[<Fact>]

let ``PlaceOrder returns error when items are empty`` () = task {

    let deps = createTestDeps ()

    let request = { CustomerId = "cust-123"; Items = [] }

    let! result = OrderService.placeOrder deps request

    test <@ Result.isError result @>

}

Parameterized Tests with Theory

[<Theory>]

[<InlineData("")>]

[<InlineData("   ")>]

let ``PlaceOrder rejects empty customer ID`` (customerId: string) =

    let request = { CustomerId = customerId; Items = [ validItem ] }

    let result = OrderService.placeOrder request

    result |> should be (ofCase <@ Error @>)

[<Theory>]

[<InlineData("", false)>]

[<InlineData("a", false)>]

[<InlineData("user@example.com", true)>]

[<InlineData("user+tag@example.co.uk", true)>]

let ``IsValidEmail returns expected result`` (email: string, expected: bool) =

    test <@ EmailValidator.isValid email = expected @>

Property-Based Testing with FsCheck

Using FsCheck.xUnit

open FsCheck

open FsCheck.Xunit

[<Property>]

let ``order total is always non-negative`` (items: NonEmptyList<PositiveInt * decimal>) =

    let orderItems =

        items.Get

        |> List.map (fun (qty, price) ->

            { Sku = "SKU"; Quantity = qty.Get; Price = abs price })

    let total = Order.calculateTotal orderItems

    total >= 0m

[<Property>]

let ``serialization roundtrips`` (order: Order) =

    let json = JsonSerializer.Serialize order

    let deserialized = JsonSerializer.Deserialize<Order> json

    deserialized = order

Custom Generators

type OrderGenerators =

    static member ValidEmail () =

        gen {

            let! user = Gen.elements [ "alice"; "bob"; "carol" ]

            let! domain = Gen.elements [ "example.com"; "test.org" ]

            return $"{user}@{domain}"

        }

        |> Arb.fromGen

[<Property(Arbitrary = [| typeof<OrderGenerators> |])>]

let ``valid emails pass validation`` (email: string) =

    EmailValidator.isValid email

Mocking Dependencies

Function Stubs (Preferred)

let createTestDeps () =

    let mutable savedOrders = []

    { FindOrder = fun id -> task { return Map.tryFind id testData }

      SaveOrder = fun order -> task { savedOrders <- order :: savedOrders }

      SendNotification = fun _ -> Task.CompletedTask }

[<Fact>]

let ``PlaceOrder saves the confirmed order`` () = task {

    let mutable saved = []

    let deps =

        { createTestDeps () with

            SaveOrder = fun order -> task { saved <- order :: saved } }

    let! _ = OrderService.placeOrder deps validRequest

    test <@ saved.Length = 1 @>

}

NSubstitute for .NET Interfaces

open NSubstitute

[<Fact>]

let ``calls repository with correct ID`` () = task {

    let repo = Substitute.For<IOrderRepository>()

    repo.FindByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())

        .Returns(Task.FromResult(Some testOrder))

    let service = OrderService(repo)

    let! _ = service.GetOrder(testOrder.Id, CancellationToken.None)

    do! repo.Received(1).FindByIdAsync(testOrder.Id, Arg.Any<CancellationToken>())

}

ASP.NET Core Integration Tests

type OrderApiTests (factory: WebApplicationFactory<Program>) =

    interface IClassFixture<WebApplicationFactory<Program>>

    let client =

        factory.WithWebHostBuilder(fun builder ->

            builder.ConfigureServices(fun services ->

                services.RemoveAll<DbContextOptions<AppDbContext>>() |> ignore

                services.AddDbContext<AppDbContext>(fun options ->

                    options.UseInMemoryDatabase("TestDb") |> ignore) |> ignore))

            .CreateClient()

    [<Fact>]

    member _.``GET order returns 404 when not found`` () = task {

        let! response = client.GetAsync($"/api/orders/{Guid.NewGuid()}")

        test <@ response.StatusCode = HttpStatusCode.NotFound @>

    }

Test Organization

tests/

  MyApp.Tests/

    Unit/

      OrderServiceTests.fs

      PaymentServiceTests.fs

    Integration/

      OrderApiTests.fs

      OrderRepositoryTests.fs

    Properties/

      OrderPropertyTests.fs

    Helpers/

      TestData.fs

      TestDeps.fs

Common Anti-Patterns

Anti-Pattern

Fix

Testing implementation details

Test behavior and outcomes

Mutable shared test state

Fresh state per test

Thread.Sleep in async tests

Use Task.Delay with timeout, or polling helpers

Asserting on sprintf output

Assert on typed values and pattern matches

Ignoring CancellationToken

Always pass and verify cancellation

Skipping property-based tests

Use FsCheck for any function with clear invariants

Related Skills

  • dotnet-patterns - Idiomatic .NET patterns, dependency injection, and architecture
  • csharp-testing - C# testing patterns (shared infrastructure like WebApplicationFactory and Testcontainers applies to F# too)

Running Tests

# Run all tests

dotnet test

# Run with coverage

dotnet test --collect:"XPlat Code Coverage"

# Run specific project

dotnet test tests/MyApp.Tests/

# Filter by test name

dotnet test --filter "FullyQualifiedName~OrderService"

# Watch mode during development

dotnet watch test --project tests/MyApp.Tests/
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card