Catalog
affaan-m/fsharp-testing

affaan-m

fsharp-testing

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

NewUpdated Sep 9, 2026

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
  • 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/
Files1
1 files · 1.0 KB

Select a file to preview

Overall Score

88/100

Grade

A

Excellent

Grades are signals, not a certification. Always review a skill yourself before use.

Safety

92

Quality

87

Clarity

85

Completeness

82

Summary

F# testing patterns skill provides comprehensive guidance for writing unit, property-based, and integration tests using xUnit, FsUnit, Unquote, and FsCheck. The skill teaches modern .NET testing practices including dependency injection with function stubs, mocking with NSubstitute, and ASP.NET Core integration testing with WebApplicationFactory, covering both synchronous and asynchronous test patterns.

Detected Capabilities

code generation (test templates)file reading (reviewing existing tests)guidance on test organization and structureshell command patterns (dotnet test execution)

Trigger Keywords

Phrases that agents use to match this skill to user intent.

write f# testsunit test setupproperty-based testingintegration testingtest dependenciesfsharp xunitasync test patternstest organization

Use Cases

  • Write unit tests for F# business logic using xUnit and FsUnit
  • Set up property-based tests with FsCheck to validate invariants and edge cases
  • Mock .NET dependencies using function stubs or NSubstitute in test doubles
  • Create integration tests for ASP.NET Core APIs using WebApplicationFactory
  • Debug flaky or slow tests by applying test organization and async best practices
  • Review test coverage and structure following organized test directory patterns

Quality Notes

  • Skill provides well-organized, practical examples covering the full testing spectrum (unit, property-based, integration)
  • Clear progression from basic assertions to advanced patterns (custom generators, async fixtures)
  • Excellent use of comparison tables to distinguish tools (xUnit vs FsUnit vs Unquote) and anti-patterns vs fixes
  • Code examples are idiomatic F# with proper syntax for xUnit attributes, FsUnit assertions, and FsCheck properties
  • Includes actionable guidance on test organization, naming conventions, and common pitfalls
  • Integration test example shows real-world complexity (WebApplicationFactory configuration, in-memory database setup)
  • Anti-patterns section is particularly valuable, bridging common mistakes to solutions
  • References related skills (`dotnet-patterns`, `csharp-testing`) for cross-domain learning
Model: claude-haiku-4-5-20251001Analyzed: Sep 9, 2026

Reviews

Add this skill to your library to leave a review.

No reviews yet

Be the first to share your experience.

Version History

  1. v2.0

    Contract changed: description

    ✦ AIClarifies skill activation: adds guidance to use the skill when writing F# tests with xUnit, FsUnit, Unquote, or FsCheck.

    triggering2026-09-09

    LATEST
  2. v1.1

    Content updated

    ✦ AINo behavioral changes detected.

    2026-07-14

    View This Version
  3. v1.0

    2026-05-15

    View This VersionInitial version

Use affaan-m/fsharp-testing in your dev environment

Command Palette

Search for a command to run...