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

举报或申请下架