PluginBench
Skill
Pass
Audit score 90

fsharp-testing

affaan-m/everything-claude-code

F# testing patterns with xUnit, FsUnit, Unquote, and FsCheck property-based testing.

What is fsharp-testing?

Comprehensive testing patterns for F# applications covering unit tests, property-based testing, mocking, and ASP.NET Core integration tests. Use this skill when writing new tests, reviewing test quality, setting up test infrastructure, or debugging flaky tests.

  • Write unit tests with xUnit and FsUnit assertion syntax
  • Create property-based tests using FsCheck with custom generators
  • Mock dependencies using function stubs or NSubstitute for .NET interfaces
  • Build ASP.NET Core integration tests with WebApplicationFactory
  • Use Unquote quotation-based assertions for clear failure messages
  • Organize tests into unit, integration, and property-based test suites

How to install fsharp-testing

npx skills add https://github.com/affaan-m/everything-claude-code --skill fsharp-testing
Claude Code
Cursor
Windsurf
Cline

How to use fsharp-testing

  1. 1.Choose your assertion library: FsUnit for readable syntax or Unquote for quotation-based messages
  2. 2.Write unit tests using [<Fact>] for single cases or [<Theory>] for parameterized tests
  3. 3.Add property-based tests with [<Property>] for functions with clear invariants
  4. 4.Mock dependencies using function stubs (preferred) or NSubstitute for .NET interfaces
  5. 5.For async code, use task { } expressions and test <@ @> assertions
  6. 6.Organize tests into Unit/, Integration/, and Properties/ folders
  7. 7.Run tests with dotnet test and use dotnet watch test during development

Use cases

Good for
  • Testing order processing logic with unit and property-based tests
  • Setting up integration tests for ASP.NET Core APIs with in-memory databases
  • Validating email and input validation functions across edge cases
  • Mocking repository and service dependencies in async workflows
  • Verifying serialization roundtrips and invariants with property tests
Who it's for
  • F# developers writing new test suites
  • Teams establishing test infrastructure for F# projects
  • Developers reviewing test quality and coverage
  • Engineers debugging flaky or slow tests

fsharp-testing FAQ

Should I use FsUnit or Unquote?

FsUnit provides readable F#-style assertions (should equal, should be). Unquote uses F# quotations to show the full expression that failed, making complex assertions clearer. Use FsUnit for simple assertions and Unquote when failure messages matter most.

How do I test async code?

Use task { } expressions with let! bindings. Mark tests with [<Fact>] and use test <@ @> assertions inside. Always pass CancellationToken parameters and verify cancellation behavior rather than using Thread.Sleep.

When should I use property-based testing?

Use FsCheck for any function with clear invariants or mathematical properties (e.g., order totals are non-negative, serialization roundtrips, list operations are commutative). Start with unit tests for specific cases, then add properties for broader coverage.

How do I mock dependencies?

Prefer function stubs by creating test versions of your dependency records. For .NET interfaces, use NSubstitute with Substitute.For<IInterface>() and configure return values and call verification.

How do I set up integration tests for ASP.NET Core?

Use WebApplicationFactory<Program> as a test fixture, override services (like DbContext) with in-memory versions, and create a client to make HTTP requests. Assert on response status codes and deserialized response bodies.

Full instructions (SKILL.md)

Source of truth, from affaan-m/everything-claude-code.


name: fsharp-testing description: F# testing patterns with xUnit, FsUnit, Unquote, FsCheck property-based testing, integration tests, and test organization best practices. metadata: origin: ECC

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

ToolPurpose
xUnitTest framework (standard .NET ecosystem choice)
FsUnit.xUnitF#-friendly assertion syntax for xUnit
UnquoteAssertion library using F# quotations for clear failure messages
FsCheck.xUnitProperty-based testing integrated with xUnit
NSubstituteMocking .NET dependencies
TestcontainersReal infrastructure in integration tests
WebApplicationFactoryASP.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-PatternFix
Testing implementation detailsTest behavior and outcomes
Mutable shared test stateFresh state per test
Thread.Sleep in async testsUse Task.Delay with timeout, or polling helpers
Asserting on sprintf outputAssert on typed values and pattern matches
Ignoring CancellationTokenAlways pass and verify cancellation
Skipping property-based testsUse 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/