fsharp-testing
How to Install
Claude Code:
git clone --depth 1 https://github.com/affaan-m/ECC.git && cp ECC/skills/fsharp-testing ~/.claude/skills/fsharp-testing -r---
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
```fsharp
module OrderServiceTests
open Xunit
open FsUnit.Xunit
[]
let ``create sets status to Pending`` () =
let order = Order.create "cust-1" [ validItem ]
order.Status |> should equal Pending
[]
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".
```fsharp
module OrderValidationTests
open Xunit
open Swensen.Unquote
[]
let ``PlaceOrder returns success when request is valid`` () =
let request = { CustomerId = "cust-123"; Items = [ validItem ] }
let result = OrderService.placeOrder request
test <@ Result.isOk result @>
[]
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 @>
[]
let ``validated email rejects empty input`` () =
let result = ValidatedEmail.create ""
test <@ Result.isError result @>
```
### Async Tests
```fsharp
[]
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 @>
}
[]
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
```fsharp
[]
[]
[]
let ``PlaceOrder rejects empty customer ID`` (customerId: string) =
let request = { CustomerId = customerId; Items = [ validItem ] }
let result = OrderService.placeOrder request
result |> should be (ofCase <@ Error @>)
[]
[]
[]
[]
[]
let ``IsValidEmail returns expected result`` (email: string, expected: bool) =
test <@ EmailValidator.isValid email = expected @>
```
## Property-Based Testing with FsCheck
### Using FsCheck.xUnit
```fsharp
open FsCheck
open FsCheck.Xunit
[]
let ``order total is always non-negative`` (items: NonEmptyList) =
let orderItems =
items.Get
|> List.map (fun (qty, price) ->
{ Sku = "SKU"; Quantity = qty.Get; Price = abs price })
let total = Order.calculateTotal orderItems
total >= 0m
[]
let ``serialization roundtrips`` (order: Order) =
let json = JsonSerializer.Serialize order
let deserialized = JsonSerializer.Deserialize json
deserialized = order
```
### Custom Generators
```fsharp
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
[ |])>]
let ``valid emails pass validation`` (email: string) =
EmailValidator.isValid email
```
## Mocking Dependencies
### Function Stubs (Preferred)
```fsharp
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 }
[]
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
```fsharp
open NSubstitute
[]
let ``calls repository with correct ID`` () = task {
let repo = Substitute.For()
repo.FindByIdAsync(Arg.Any(), Arg.Any())
.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())
}
```
## ASP.NET Core Integration Tests
```fsharp
type OrderApiTests (factory: WebApplicationFactory) =
interface IClassFixture>
let client =
factory.WithWebHostBuilder(fun builder ->
builder.ConfigureServices(fun services ->
services.RemoveAll>() |> ignore
services.AddDbContext(fun options ->
options.UseInMemoryDatabase("TestDb") |> ignore) |> ignore))
.CreateClient()
[]
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
```bash
# 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/
```
Details
| Category | Coding → generation |
| Source | affaan-m/ECC |
| SKILL.md | View on GitHub → |
| Repo Stars | ★ 220.5K |
| Est. per Skill | N/A (shared across 121 skills from this repo) |
| Difficulty | Intermediate |
| Risk Level | Safe |
Related Skills
pubmed-database
PubMed Database Overview PubMed is the U.S. National Library of Medicine's comprehensive database pr
shopify-apps
Shopify Apps Expert patterns for Shopify app development including Remix/React Router apps, embedded
deployment-validation-config-validate
Configuration Validation You are a configuration management expert specializing in validating, testi
api-fuzzing-bug-bounty
AUTHORIZED USE ONLY: Use this skill only for authorized security assessments, defensive validation,
Works Well With
Skills from the same repository — often designed to work together
accessibility
Accessibility (WCAG 2.2) This skill ensures that digital interfaces are Perceivable, Operable, Under
agent-architecture-audit
Agent Architecture Audit A diagnostic workflow for agent systems that hide failures behind wrapper l
agent-eval
Agent Eval Skill A lightweight CLI tool for comparing coding agents head-to-head on reproducible tas