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-testingHow to use fsharp-testing
- 1.Choose your assertion library: FsUnit for readable syntax or Unquote for quotation-based messages
- 2.Write unit tests using [<Fact>] for single cases or [<Theory>] for parameterized tests
- 3.Add property-based tests with [<Property>] for functions with clear invariants
- 4.Mock dependencies using function stubs (preferred) or NSubstitute for .NET interfaces
- 5.For async code, use task { } expressions and test <@ @> assertions
- 6.Organize tests into Unit/, Integration/, and Properties/ folders
- 7.Run tests with dotnet test and use dotnet watch test during development
Use cases
- 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
- 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
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.
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.
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.
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.
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
| 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 architecturecsharp-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/
Related skills
More from affaan-m/everything-claude-code and the wider catalog.

gan-style-harness
Multi-agent harness with separate Generator and Evaluator agents for autonomous, high-quality application development.

gateguard
Fact-forcing gate that blocks edits and demands concrete investigation before allowing code changes.

gget
Quick genomic database queries, BLAST searches, and reproducible bioinformatics evidence logs via gget CLI.

git-workflow
Git workflow patterns, branching strategies, and collaborative development best practices.

github-ops
Manage GitHub issues, PRs, CI/CD, releases, and security using gh CLI.

golang-patterns
Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable applications.