Fsharp Testing

affaan-m/ECC/skills/fsharp-testing

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775無授權條款275K 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫4 天前更新

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.

AI 產生的概覽

使用 xUnit、FsUnit、Unquote、FsCheck 屬性測試與整合測試的 F# 測試模式。

功能
提供撰寫 F# 測試的參考模式:使用 xUnit 與 FsUnit 的單元測試、以引用為基礎的 Unquote 斷言、非同步測試、參數化 Theory 測試,以及包含自訂產生器的 FsCheck 屬性測試。內容也涵蓋以函式樁與 NSubstitute 進行的模擬、透過 WebApplicationFactory 的 ASP.NET Core 整合測試、建議的測試目錄結構、常見反模式,以及 dotnet test 指令。
適用情境
適用於為 F# 程式碼撰寫新測試、檢視測試品質與涵蓋率、為 F# 專案建立測試基礎設施,或偵錯不穩定或緩慢的測試。
執行需求
需要 F#/.NET 工具鏈,並依所述模式使用 xUnit、FsUnit.xUnit、Unquote、FsCheck.xUnit、NSubstitute、Testcontainers 與 WebApplicationFactory;不附帶指令碼,僅提供說明。

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

fsharp
module OrderServiceTests
open Xunitopen 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 Xunitopen 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("[email protected]", true)>][<InlineData("[email protected]", 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 FsCheckopen 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-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

bash
# Run all testsdotnet test
# Run with coveragedotnet test --collect:"XPlat Code Coverage"
# Run specific projectdotnet test tests/MyApp.Tests/
# Filter by test namedotnet test --filter "FullyQualifiedName~OrderService"
# Watch mode during developmentdotnet watch test --project tests/MyApp.Tests/

來源與署名

來源:affaan-m/ECC位於skills/fsharp-testing提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架