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 从公开仓库中收录这些内容。

举报或申请下架