Communitygithub.com

fsharp-testing

xUnit, FsUnit, Unquote, FsCheck 속성 기반 테스트, 통합 테스트 및 테스트 조직 모범 사례를 포함한 F# 테스트 패턴입니다. F# 코드를 xUnit, FsUnit, Unquote 또는 FsCheck로 테스트할 때 사용하세요.

fsharp-testing란 무엇인가요?

이 스킬은 F# 애플리케이션에 대한 포괄적인 테스트 패턴 가이드입니다. xUnit, FsUnit, Unquote, FsCheck 등 최신 .NET 테스트 도구를 활용하며, 단위 테스트, 속성 기반 테스트, 모킹, ASP.NET Core 통합 테스트 전반을 다룹니다. 특히 Unquote의 F# 인용(quotation) 기반 명확한 실패 메시지와 FsCheck의 속성 기반 테스트를 통해 버그 조기 발견과 코드 신뢰성을 높이고, NSubstitute 및 Testcontainers를 이용한 의존성 모킹과 실제 인프라 통합 테스트 방법도 포함합니다. 코드 예제는 모두 F#으로 작성되어 있어, F# 개발자에게 바로 적용 가능합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/fsharp-testing

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

F# 테스트 패턴

xUnit, FsUnit, Unquote, FsCheck 및 최신 .NET 테스트 관행을 사용한 F# 애플리케이션의 포괄적인 테스트 패턴입니다.

활성화 시점

  • F# 코드에 대한 새 테스트 작성 시
  • 테스트 품질 및 커버리지 검토 시
  • F# 프로젝트의 테스트 인프라 설정 시
  • 불안정하거나 느린 테스트 디버깅 시

테스트 프레임워크 스택

도구목적
xUnit테스트 프레임워크 (표준 .NET 에코시스템 선택)
FsUnit.xUnitxUnit을 위한 F# 친화적 단언 구문
Unquote명확한 실패 메시지를 위한 F# 인용(quotation) 기반 단언 라이브러리
FsCheck.xUnitxUnit에 통합된 속성 기반 테스트
NSubstitute.NET 의존성 모킹
Testcontainers통합 테스트에서의 실제 인프라
WebApplicationFactoryASP.NET Core 통합 테스트

xUnit + FsUnit을 사용한 단위 테스트

기본 테스트 구조

module OrderServiceTests

open Xunit
open FsUnit.Xunit

[<Fact>]
let ``create는 상태를 Pending으로 설정한다`` () =
    let order = Order.create "cust-1" [ validItem ]
    order.Status |> should equal Pending

[<Fact>]
let ``confirm은 상태를 Confirmed로 변경한다`` () =
    let order = Order.create "cust-1" [ validItem ]
    let confirmed = Order.confirm order
    confirmed.Status |> should be (ofCase <@ Confirmed @>)

Unquote를 사용한 단언

Unquote는 F# 인용(quotation)을 사용하므로 실패 메시지에 "expected X got Y" 대신 실패한 전체 표현식이 표시됩니다.

module OrderValidationTests

open Xunit
open Swensen.Unquote

[<Fact>]
let ``PlaceOrder는 요청이 유효할 때 성공을 반환한다`` () =
    let request = { CustomerId = "cust-123"; Items = [ validItem ] }
    let result = OrderService.placeOrder request
    test <@ Result.isOk result @>

[<Fact>]
let ``주문 총액은 상품 가격의 합이다`` () =
    let items = [ { Sku = "A"; Quantity = 2; Price = 10m }
                  { Sku = "B"; Quantity = 1; Price = 5m } ]
    let total = Order.calculateTotal items
    test <@ total = 25m @>

[<Fact>]
let ``검증된 이메일은 빈 입력을 거부한다`` () =
    let result = ValidatedEmail.create ""
    test <@ Result.isError result @>

비동기 테스트

[<Fact>]
let ``PlaceOrder는 요청이 유효할 때 성공을 반환한다`` () = task {
    let deps = createTestDeps ()
    let request = { CustomerId = "cust-123"; Items = [ validItem ] }

    let! result = OrderService.placeOrder deps request

    test <@ Result.isOk result @>
}

[<Fact>]
let ``PlaceOrder는 항목이 비어 있을 때 오류를 반환한다`` () = task {
    let deps = createTestDeps ()
    let request = { CustomerId = "cust-123"; Items = [] }

    let! result = OrderService.placeOrder deps request

    test <@ Result.isError result @>
}

Theory를 사용한 매개변수화된 테스트

[<Theory>]
[<InlineData("")>]
[<InlineData("   ")>]
let ``PlaceOrder는 빈 고객 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은 예상된 결과를 반환한다`` (email: string, expected: bool) =
    test <@ EmailValidator.isValid email = expected @>

FsCheck를 사용한 속성 기반 테스트

FsCheck.xUnit 사용

open FsCheck
open FsCheck.Xunit

[<Property>]
let ``주문 총액은 항상 음수가 아니다`` (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 ``직렬화가 라운드트립된다`` (order: Order) =
    let json = JsonSerializer.Serialize order
    let deserialized = JsonSerializer.Deserialize<Order> json
    deserialized = order

사용자 정의 생성기

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 ``유효한 이메일은 검증을 통과한다`` (email: string) =
    EmailValidator.isValid email

의존성 모킹

함수 스텁 (권장)

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는 확인된 주문을 저장한다`` () = task {
    let mutable saved = []
    let deps =
        { createTestDeps () with
            SaveOrder = fun order -> task { saved <- order :: saved } }

    let! _ = OrderService.placeOrder deps validRequest

    test <@ saved.Length = 1 @>
}

.NET 인터페이스를 위한 NSubstitute

open NSubstitute

[<Fact>]
let ``올바른 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 통합 테스트

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는 찾을 수 없을 때 404를 반환한다`` () = task {
        let! response = client.GetAsync($"/api/orders/{Guid.NewGuid()}")
        test <@ response.StatusCode = HttpStatusCode.NotFound @>
    }

테스트 조직

tests/
  MyApp.Tests/
    Unit/
      OrderServiceTests.fs
      PaymentServiceTests.fs
    Integration/
      OrderApiTests.fs
      OrderRepositoryTests.fs
    Properties/
      OrderPropertyTests.fs
    Helpers/
      TestData.fs
      TestDeps.fs

일반적인 안티 패턴

안티 패턴수정 방법
구현 세부 사항 테스트동작과 결과를 테스트
변경 가능한 공유 테스트 상태테스트마다 새로운 상태 사용
비동기 테스트에서 Thread.SleepTask.Delay와 타임아웃 또는 폴링 헬퍼 사용
sprintf 출력에 대한 단언타입이 있는 값과 패턴 매칭에 대해 단언
CancellationToken 무시항상 취소를 전달하고 확인
속성 기반 테스트 건너뛰기명확한 불변량이 있는 모든 함수에 FsCheck 사용

관련 스킬

  • dotnet-patterns - 관용적인 .NET 패턴, 의존성 주입 및 아키텍처
  • csharp-testing - C# 테스트 패턴 (WebApplicationFactory 및 Testcontainers와 같은 공유 인프라는 F#에도 적용됨)

테스트 실행

# 모든 테스트 실행
dotnet test

# 커버리지 포함 실행
dotnet test --collect:"XPlat Code Coverage"

# 특정 프로젝트 실행
dotnet test tests/MyApp.Tests/

# 테스트 이름으로 필터링
dotnet test --filter "FullyQualifiedName~OrderService"

# 개발 중 감시 모드
dotnet watch test --project tests/MyApp.Tests/

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬