Fsharp Testing

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

F#テストフレームワーク、プロパティベーステスト、および関数型アプローチ。

AI 產生的概覽

指導使用 xUnit、FsUnit、Unquote、FsCheck 與 .NET 整合測試來撰寫及審查 F# 測試。

功能
此技能提供 F# 測試模式,涵蓋使用 xUnit 與 FsUnit 的單元測試、以引號為基礎的 Unquote 斷言、FsCheck 屬性測試、透過函式替身或 NSubstitute 進行模擬,以及使用 WebApplicationFactory 的 ASP.NET Core 整合測試。它也提供測試資料夾結構建議、常見反模式與修正方式,以及用於執行、篩選與收集涵蓋率的 dotnet test 指令。
適用情境
適用於為 F# 程式碼撰寫新測試、審查測試品質與涵蓋率、為 F# 專案建置測試基礎設施,或偵錯不穩定或緩慢的測試。
執行需求
不包含指令碼,僅為說明性內容。所述模式假定具備 .NET 工具鏈與 dotnet test,以及 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位於docs/ja-JP/skills/fsharp-testing提交ef648e0

授權條款: 無授權條款

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

檢舉或申請下架