mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 08:28:39 +08:00
docs: salvage F# agent and language guidance
This commit is contained in:
committed by
Affaan Mustafa
parent
a8836d7bbd
commit
fd9453f6ee
280
skills/fsharp-testing/SKILL.md
Normal file
280
skills/fsharp-testing/SKILL.md
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
name: fsharp-testing
|
||||
description: F# testing patterns with xUnit, FsUnit, Unquote, FsCheck property-based testing, integration tests, and test organization best practices.
|
||||
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
|
||||
|
||||
[<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".
|
||||
|
||||
```fsharp
|
||||
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
|
||||
|
||||
```fsharp
|
||||
[<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
|
||||
|
||||
```fsharp
|
||||
[<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
|
||||
|
||||
```fsharp
|
||||
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
|
||||
|
||||
```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
|
||||
|
||||
[<Property(Arbitrary = [| typeof<OrderGenerators> |])>]
|
||||
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 }
|
||||
|
||||
[<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
|
||||
|
||||
```fsharp
|
||||
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
|
||||
|
||||
```fsharp
|
||||
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
|
||||
|
||||
```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/
|
||||
```
|
||||
Reference in New Issue
Block a user