Integration Testing ohne UI: Wie ich YNAB und Comdirect APIs testbar machte
Einleitung
Als ich heute mit der Implementierung der BudgetBuddy-Integrations-Tests begann, stand ich vor einem klassischen Problem: Wie teste ich externe API-Integrationen, wenn das UI noch nicht fertig ist?
Die Herausforderung war konkret: Ich hatte zwei komplexe API-Integrationen implementiert:
- YNAB API: Budgets, Accounts, Categories abrufen
- Comdirect OAuth: Komplexer Flow mit Push-TAN-Bestätigung
Beide waren im Backend fertig implementiert, aber die Frontend-Views existierten noch nicht. Ein klassisches Henne-Ei-Problem: Ohne UI kann ich nicht manuell testen, aber ohne Tests weiß ich nicht, ob die APIs überhaupt funktionieren.
Die zweite Herausforderung war CI/CD-Freundlichkeit: Integration-Tests sollten nicht bei jedem dotnet test laufen und Push-TANs an mein Handy senden. Aber sie sollten trotzdem leicht ausführbar sein, wenn ich sie brauche.
In diesem Blogpost beschreibe ich, wie ich eine flexible Test-Infrastruktur gebaut habe, die zwei Szenarien unterstützt:
- Schnelle Unit-Tests (default, keine API-Calls)
- Opt-in Integration-Tests (mit echten API-Calls)
Für manuelle End-to-End-Tests (z.B. Comdirect OAuth mit Push-TAN) gibt es F# Scripts (scripts/test-comdirect.fsx), die interaktiv mit dotnet fsi ausgeführt werden können.
Ausgangslage
BudgetBuddy hatte bereits eine solide Test-Basis mit 88 Tests:
- Unit-Tests für Decoder (YNAB, Comdirect)
- Property-Based Tests mit FsCheck
- Type-Conversion-Tests für Persistence
- Encryption-Tests
Was fehlte:
- ❌ Keine Tests mit echten API-Calls
- ❌ Keine Möglichkeit, OAuth-Flow manuell zu testen
- ❌ Keine Transaction-Decoder-Tests
- ❌ Kein Weg, Credentials einfach zu verwalten
Die bestehenden Tests waren pure Unit-Tests: Sie testeten Decoder mit Mock-JSON, aber nie die tatsächliche API-Kommunikation. Das war gut für schnelle Feedback-Loops, aber ich wusste nicht, ob der Code wirklich funktioniert.
Herausforderung 1: Credentials ohne Umgebungsvariablen
Das Problem
Die Test-Scripts und Integration-Tests brauchten API-Credentials. Die naive Lösung wäre:
YNAB_TOKEN=xxx COMDIRECT_USERNAME=yyy dotnet test
Warum das nervig ist:
- Jedes Mal die Credentials tippen
- Credentials in Shell-History
- Fehleranfällig (Tippfehler)
- Keine gute Developer Experience
Optionen, die ich betrachtet habe
- Hardcoded in Test-Dateien
- ✅ Pro: Einfach
- ❌ Contra: NIEMALS Credentials committen!
- ❌ Contra: Jeder Entwickler hat andere Credentials
- Umgebungsvariablen vor jedem Test setzen
- ✅ Pro: Standard-Approach
- ❌ Contra: Umständlich, fehleranfällig
- ❌ Contra: Shell-History-Problem
- .env File (gewählt)
- ✅ Pro:
.gitignored, sicher - ✅ Pro: Einmal setzen, immer nutzen
- ✅ Pro: Alle Tools (Scripts + Tests) nutzen die gleiche Quelle
- ❌ Contra: Braucht einen Parser
- ✅ Pro:
Die Lösung: EnvLoader.fsx
Ich habe ein wiederverwendbares F# Script-Modul erstellt:
// scripts/EnvLoader.fsx
module EnvLoader
let loadEnv (envPath: string) =
File.ReadAllLines(envPath)
|> Array.filter (fun line ->
not (String.IsNullOrWhiteSpace(line)) &&
not (line.TrimStart().StartsWith("#"))
)
|> Array.choose (fun line ->
match line.Split('=', 2) with
| [| key; value |] -> Some (key.Trim(), value.Trim())
| _ -> None
)
|> Map.ofArray
let getRequired (envVars: Map<string, string>) (key: string) =
match envVars.TryFind key with
| Some value -> value
| None -> failwith $"Missing required: {key}"
Nutzung in Tests:
// src/Tests/YnabIntegrationTests.fs
let private loadEnv () =
let envPath = Path.Combine(projectRoot, ".env")
File.ReadAllLines(envPath)
|> // ... gleiche Logik
Nutzung in Scripts:
// scripts/test-ynab.fsx
#load "EnvLoader.fsx"
let env = EnvLoader.loadProjectEnv()
let token = EnvLoader.getRequired env "YNAB_TOKEN"
Architekturentscheidung: Warum kein NuGet-Package?
Ich hätte dotenv-net oder ähnliche Packages nutzen können. Warum nicht?
- Zero Dependencies für Scripts: F# Scripts sollen ohne komplexe Setup laufen
- Einfachheit: 30 Zeilen Code vs. NuGet-Abhängigkeit
- Lerneffekt: Parsing ist trivial, zeigt F#-Patterns
- Kontrolle: Ich kann Secret-Masking hinzufügen (zeige nur
YNAB_...ff3f)
Trade-off: Kein vollständiger .env-Parser (z.B. keine Quotes, keine Variable-Substitution). Für BudgetBuddy ausreichend - wir haben simple Key=Value-Paare.
Herausforderung 2: ComdirectSettings hatte falsches Design
Das Problem
Als ich den OAuth-Test schrieb, bekam ich diesen Fehler:
❌ FAILED: Could not start OAuth flow
Error: AuthenticationFailed "Bad client credentials"
Beim Debuggen fand ich den Bug in ComdirectClient.fs:
// FALSCH! ❌
let body =
sprintf "client_id=%s&client_secret=%s&username=%s&password=%s&grant_type=password"
apiKeys.ClientId
apiKeys.ClientSecret
credentials.AccountId // ← AccountID als Username! 🤦
"password_placeholder" // ← Hardcoded! 🤦
Warum dieser Bug unentdeckt blieb
Der Code kompilierte type-safe, aber war semantisch falsch:
AccountIdist eine Account-Nummer (z.B.9403EAA32D3F473F...)Usernameist die Benutzerkennung (z.B.90470934)
F# kann nicht wissen, dass ich hier das falsche Feld nutze - beide sind string.
Die Lösung: ComdirectSettings erweitert
Ich habe den Domain-Type gefixt:
// src/Shared/Domain.fs
type ComdirectSettings = {
ClientId: string
ClientSecret: string
Username: string // NEU ✅
Password: string // NEU ✅
AccountId: string option // JETZT OPTIONAL ✅
}
Warum AccountId: string option?
Die AccountId wird nur für Transaction-Fetching gebraucht, nicht für OAuth:
- OAuth-Flow: Braucht Username + Password
- Transaction-Fetch: Braucht zusätzlich AccountId
Das macht den Type ehrlich: Er zeigt, dass AccountId optional ist.
Lessons Learned: Type Safety ≠ Semantic Correctness
F# ist type-safe, aber ich muss trotzdem semantisch korrekte Types wählen:
Schlecht (beide string):
type Settings = {
AccountId: string
Username: string
}
Besser (Single-Case Unions):
type AccountId = AccountId of string
type Username = Username of string
type Settings = {
AccountId: AccountId
Username: Username
}
Dann kann ich nicht versehentlich AccountId und Username verwechseln!
Trade-off: Für BudgetBuddy habe ich die einfache Version gewählt (plain strings), weil:
- Weniger Boilerplate
- Klare Naming (Username vs. AccountId ist eindeutig)
- Keine Verwechslungsgefahr in der kleinen Codebase
In einer größeren Codebase würde ich Single-Case Unions nutzen.
Herausforderung 3: Transaction Decoder - Kann keine Functions serialisieren
Das Problem
Nach dem OAuth-Fix bekam ich einen kryptischen Runtime-Error:
Error: NetworkError (0, "Cannot generate auto encoder for
Microsoft.FSharp.Core.FSharpFunc`2[[...
Die volle Fehlermeldung war ~800 Zeichen lang - klassisches “F# Type System explodiert”-Problem.
Root Cause Analysis
Der Bug war in ComdirectClient.fs, Zeile 103:
let private transactionDecoder: Decoder<BankTransaction> =
Decode.object (fun get ->
// ... Felder dekodieren ...
let rawData = Encode.Auto.toString(0, get.Required.Raw) // ❌ BUG!
{ /* ... */ RawData = rawData }
)
Was ist get.Required.Raw?
Raw ist ein internes Thoth.Json-Objekt mit Decoder-Functions:
- Type:
Decoder<'T>(eine Function!) - Zweck: Ermöglicht Zugriff auf rohes JToken
Warum crasht Encode.Auto.toString?
Encode.Auto nutzt Reflection, um jeden F#-Type zu serialisieren. Aber Functions sind nicht serialisierbar!
Encode.Auto.toString(0, fun x -> x + 1) // ❌ Crash!
Die Lösung: RawData als leerer String
{
Id = transactionId
BookingDate = bookingDate
Amount = { Amount = amountValue; Currency = currency }
Payee = payee
Memo = memo
Reference = reference
RawData = "" // ✅ TODO: Store raw JSON if needed
}
Warum nicht das rohe JSON speichern?
Ich könnte das JSON speichern:
// Option 1: JSON als String
let! content = response.Content.ReadAsStringAsync()
let rawData = content
Aber wo speichere ich es? Ich habe nur Zugriff auf das dekodierte Objekt, nicht auf den Response-String.
Bessere Lösung (für später):
let private handleResponse (decoder: Decoder<'T>) (response: HttpResponseMessage) =
async {
let! content = response.Content.ReadAsStringAsync()
match Decode.fromString decoder content with
| Ok value ->
// Hier könnte ich content in 'value' speichern
return Ok value
| Error err -> return Error err
}
Trade-off: Für jetzt speichere ich kein RawData. Wenn ich später Debugging brauche, kann ich es hinzufügen.
Lessons Learned: Thoth.Json Decoder sind nicht serialisierbar
Regel: Nutze nie Encode.Auto auf Decoder-internen Objekten!
// ❌ FALSCH
get.Required.Raw // Enthält Functions
// ✅ RICHTIG
get.Required.Field "foo" Decode.string // Dekodierter Wert
Herausforderung 4: Integration Tests stören CI/CD
Das Problem
Ich hatte Integration-Tests geschrieben:
testCase "can fetch budgets with real token" <| fun () ->
match getEnvVar "YNAB_TOKEN" with
| None -> Tests.skiptest "No token"
| Some token ->
async {
let! result = getBudgets token
// ... assertions ...
} |> Async.RunSynchronously
Beim Ausführen:
$ dotnet test
# ⚠️ Sendet Push-TAN an mein Handy!
# ⚠️ Macht echte YNAB API-Calls!
# ⚠️ Konsumiert API Rate-Limits!
Warum das schlecht ist:
- CI/CD: GitHub Actions hat keine
.env→ Tests schlagen fehl - Entwickler-Nerv: Bei jedem
dotnet testPush-TAN bekommen - Rate-Limits: YNAB API hat Limits (200 requests/hour)
- Performance: Integration-Tests sind langsam (~3 Sekunden)
Optionen, die ich betrachtet habe
- Separate Test-Projekte
- ✅ Pro: Klare Trennung
- ❌ Contra: Mehr Boilerplate, mehr Dateien
- ❌ Contra:
dotnet testläuft trotzdem alle
- Test-Categories/Tags
- ✅ Pro:
dotnet test --filter Category!=Integration - ❌ Contra: Expecto nutzt keine Test-Categories (ist kein xUnit)
- ❌ Contra: Komplizierte Filter-Syntax
- ✅ Pro:
- Environment-Variable-Flag (gewählt)
- ✅ Pro: Einfach zu verstehen
- ✅ Pro: Funktioniert in allen Tools
- ✅ Pro: Selbst-dokumentierend
- ❌ Contra: Noch eine Umgebungsvariable
Die Lösung: RUN_INTEGRATION_TESTS Flag
// src/Tests/YnabIntegrationTests.fs
let private shouldRunIntegrationTests () =
match getEnvVar "RUN_INTEGRATION_TESTS" with
| Some value when value.ToLower() = "true" -> true
| _ -> false
testCase "can fetch budgets with real token" <| fun () ->
if not (shouldRunIntegrationTests()) then
Tests.skiptest "RUN_INTEGRATION_TESTS not set"
// ... Test-Code ...
Nutzung:
# Standard (keine Integration-Tests)
$ dotnet test
# ✅ 82 Tests passed, 6 skipped
# Mit Integration-Tests
$ RUN_INTEGRATION_TESTS=true dotnet test
# ✅ 88 Tests passed, 0 skipped (mit echten API-Calls!)
# Oder in .env setzen:
$ echo "RUN_INTEGRATION_TESTS=true" >> .env
$ dotnet test
Architekturentscheidung: Warum Skip statt Separate Projects?
Alternative: Ich hätte ein Tests.Integration.fsproj Projekt erstellen können:
dotnet test src/Tests # Nur Unit-Tests
dotnet test src/Tests.Integration # Integration-Tests
Warum ich das NICHT gemacht habe:
- Developer Experience:
dotnet testsoll “einfach funktionieren” - Sichtbarkeit: Skipped Tests zeigen, dass Integration-Tests existieren
- Flexibilität: Ich kann einzelne Tests aktivieren
- Weniger Boilerplate: Keine duplizierte .fsproj, PackageReferences, etc.
Trade-off: Skip-Tests erscheinen im Output (6 skipped). Das ist gewollt - es erinnert mich, dass Integration-Tests existieren!
Herausforderung 5: Vollständiger OAuth Flow ist nicht automatisierbar
Das Problem
Push-TAN ist manuell - ich muss auf meinem Handy bestätigen. Aber automatisierte Tests können nicht auf menschliche Interaktion warten. Console.ReadLine() funktioniert in dotnet test nicht - die Tests laufen als Batch-Prozess ohne echte Konsolen-Interaktion.
Die Lösung: F# Scripts für manuelle Tests
Statt interaktiver Tests in der Test-Suite nutze ich F# Scripts:
$ dotnet fsi scripts/test-comdirect.fsx
Das Script hat eine echte Konsole und kann auf Benutzereingaben warten. Der automatisierte Integration-Test testet nur bis zur TAN-Challenge - alles darüber hinaus muss manuell mit dem Script getestet werden.
Fazit: Interaktive Tests gehören nicht in eine automatisierte Test-Suite. Sie gehören in Scripts, die explizit manuell ausgeführt werden.
Herausforderung 6: String-Interpolation in F# kann überraschend sein
Das Problem
Beim Schreiben der Tests bekam ich diesen Compiler-Error:
error FS3373: Invalid interpolated string. Single quote or verbatim
string literals may not be used in interpolated expressions
Der Code:
printfn $"Sample: {firstTx.BookingDate:yyyy-MM-dd}"
// ^ Problem!
Warum crasht das?
F# String-Interpolation parsed den String und sieht:
{firstTx.BookingDate:yyyy-MM-dd}- Format-String:
yyyy-MM-dd - Problem: Das
-wird als Operator interpretiert!
F# denkt: “Du willst yyyy minus MM minus dd rechnen?”
Die Lösung: DateTime.ToString() in separate Variable
// ❌ FALSCH
printfn $"Sample: {firstTx.BookingDate:yyyy-MM-dd}"
// ✅ RICHTIG
let dateStr = firstTx.BookingDate.ToString("yyyy-MM-dd")
printfn $"Sample: {dateStr}"
Lessons Learned: F# String-Interpolation-Tricks
Regel: In $"...{expr}..." muss expr ein gültiger F#-Ausdruck sein.
// ✅ OK: Simple Expressions
$"Hello {name}"
$"Count: {items.Length}"
$"Sum: {x + y}"
// ❌ FEHLER: Format-Strings mit Sonderzeichen
$"Date: {date:yyyy-MM-dd}" // `-` als Operator!
$"Price: {price:C}" // `C` als Format-Specifier geht manchmal
// ✅ LÖSUNG: ToString() nutzen
let formatted = date.ToString("yyyy-MM-dd")
$"Date: {formatted}"
Alternative: Triple-Quoted Strings (für komplexe Fälle):
$"""Date: {date.ToString("yyyy-MM-dd")}"""
Herausforderung 7: Testing ohne UI - F# Scripts als Lösung
Das Problem
Ich brauchte einen Weg, die APIs interaktiv zu explorieren:
- “Funktioniert YNAB überhaupt?”
- “Welche Budgets gibt es?”
- “Wie sieht die Transaction-Response aus?”
Unit-Tests sind dafür zu rigide:
- Feste Assertions
- Kein Explorieren
- Kein “mal eben ausprobieren”
Die Lösung: F# Scripts (.fsx)
F# Scripts sind wie REPL, aber mit Files:
// scripts/test-ynab.fsx
#r "nuget: FsHttp, 14.5.1"
#load "../src/Shared/Domain.fs"
#load "../src/Server/YnabClient.fs"
open Server.YnabClient
let token = EnvLoader.getRequired env "YNAB_TOKEN"
printfn "Fetching budgets..."
let budgetsResult = getBudgets token |> Async.RunSynchronously
match budgetsResult with
| Ok budgets ->
printfn $"Found {budgets.Length} budgets:"
for budget in budgets do
printfn $" - {budget.Name}"
Ausführen:
$ dotnet fsi scripts/test-ynab.fsx
Fetching budgets...
Found 3 budgets:
- Haus
- My Budget (Archived)
- My Budget
Warum F# Scripts besser sind als Unit-Tests
| Feature | Unit-Tests | F# Scripts |
|---|---|---|
| Explorieren | ❌ Feste Assertions | ✅ Frei experimentieren |
| Output | ❌ Nur Pass/Fail | ✅ Sehe echte Daten |
| Iteration | ❌ Test schreiben, Build, Run | ✅ Script ändern, Run |
| Setup | ❌ Test-Framework nötig | ✅ Nur dotnet fsi |
| Sharing | ❌ In Tests “versteckt” | ✅ Script = Doku |
Architekturentscheidung: Scripts im Repo committen
Ich habe die Scripts im scripts/ Ordner committed:
scripts/
├── EnvLoader.fsx # Shared helper
├── test-ynab.fsx # YNAB API tester
├── test-comdirect.fsx # Comdirect OAuth tester
├── debug-comdirect-auth.fsx # Credential debugger
└── README.md # Documentation
Warum committen?
- Dokumentation: Scripts zeigen, wie die APIs benutzt werden
- Onboarding: Neue Entwickler können Scripts ausführen
- Regression-Testing: Manuell, aber schneller als UI
- Troubleshooting: Wenn etwas bricht, kann ich mit Scripts debuggen
Trade-off: Scripts können veralten. Lösung: Behandle sie wie Tests - wenn API ändert, Script updaten.
Lessons Learned
1. Type Safety ist nicht genug - Semantik zählt
F# verhindert Type-Errors, aber nicht Logik-Errors:
AccountId: stringundUsername: stringsind beidestring- Compiler kann nicht wissen, dass ich sie verwechselt habe
Lösung: Single-Case Unions für wichtige Domain-Concepts:
type AccountId = AccountId of string
type Username = Username of string
Dann kann ich sie nicht verwechseln!
2. Integration-Tests brauchen gute Developer Experience
Schlecht:
$ dotnet test
# ⚠️ Push-TAN an Handy!
# ⚠️ Macht echte API-Calls!
# Warum? 🤔
Gut:
$ dotnet test
# ✅ 82 tests passed, 6 skipped
# Hinweis: "Set RUN_INTEGRATION_TESTS=true for integration tests"
Best Practices:
- Integration-Tests opt-in by default
- Klare Dokumentation im Skip-Message
- Interaktive Tests deutlich markieren (
INTERACTIVEim Namen)
3. F# Scripts sind unterschätzt
Scripts sind nicht nur für Prototyping! Sie sind auch:
- Lebende Dokumentation (zeigen API-Nutzung)
- Onboarding-Tools (neue Entwickler können explorieren)
- Troubleshooting (schneller als UI rebuilden)
Investiere in gute Scripts:
- Klare Print-Statements
- Error-Handling
- README.md mit Examples
4. Interaktive Tests gehören in Scripts, nicht in die Test-Suite
Console.ReadLine() funktioniert nicht in dotnet test - Tests laufen als Batch-Prozess. Für manuelle Tests mit Benutzerinteraktion (z.B. Push-TAN-Bestätigung) nutze F# Scripts:
$ dotnet fsi scripts/test-comdirect.fsx
Scripts haben eine echte Konsole und können auf Eingaben warten.
5. .env Files sind King für lokale Entwicklung
Umgebungsvariablen sind gut für Produktion, aber nervig für Entwicklung:
- Zu viele Vars zu setzen
- Shell-History-Problem
- Fehleranfällig
.env Files lösen das:
- Einmal setzen, immer nutzen
.gitignored, sicher- Alle Tools nutzen die gleiche Source
Aber: Schreibe einen simplen Parser selbst (30 Zeilen) statt NuGet-Package!
Fazit
Heute habe ich eine vollständige Integration-Test-Infrastruktur für BudgetBuddy gebaut:
Was wurde erreicht:
Neue Dateien (7):
scripts/EnvLoader.fsx- .env File Parserscripts/test-ynab.fsx- YNAB API Tester (110 Zeilen)scripts/test-comdirect.fsx- Comdirect OAuth Tester (223 Zeilen)scripts/debug-comdirect-auth.fsx- Credential Debuggerscripts/README.md- Vollständige Dokumentationsrc/Tests/YnabIntegrationTests.fs- 6 Integration-Testssrc/Tests/ComdirectIntegrationTests.fs- Integration-Tests (ohne interaktive Tests)src/Tests/ComdirectDecoderTests.fs- 9 Decoder-Tests
Geänderte Dateien (6):
.env.example- YNAB + Comdirect Credentials + RUN_INTEGRATION_TESTS FlagREADME.md- Umfangreicher Testing-Abschnitt (70+ Zeilen)src/Shared/Domain.fs- ComdirectSettings erweitert (Username, Password, AccountId optional)src/Server/ComdirectClient.fs- Bug-Fix (Username statt AccountId, Transaction Decoder)src/Tests/Tests.fsproj- 2 neue Test-Filesdiary/development.md- 2 detaillierte Einträge
Test-Statistiken:
- Vorher: 88 Tests (alles Unit-Tests)
- Nachher: 97 Tests (82 Unit + 7 Integration + 9 Decoder)
- Integration-Tests: Opt-in via
RUN_INTEGRATION_TESTS=true - Scripts: 3 interaktive Test-Scripts
Bug-Fixes:
- AccountID wurde fälschlicherweise als Username verwendet → Domain-Type gefixt
- Transaction Decoder versuchte F# Functions zu serialisieren → RawData entfernt
- Integration-Tests liefen by default → Opt-in Flag hinzugefügt
- String-Interpolation Bugs (
yyyy-MM-dd) → DateTime.ToString() in Variable
Architektur-Verbesserungen:
- .env File Support für alle Tools
- Klare Trennung: Unit-Tests (schnell) vs. Integration-Tests (opt-in)
- F# Scripts als lebende Dokumentation für manuelle Tests (Push-TAN)
Test-Coverage jetzt:
| Kategorie | Anzahl | Beschreibung |
|---|---|---|
| Unit-Tests | 82 | Schnell, keine I/O, immer |
| Integration-Tests | 7 | Echte API-Calls, opt-in |
| Decoder-Tests | 9 | JSON-Struktur-Validierung |
| Property-Based | 3 | FsCheck Edge-Cases |
| Total | 97 | Vollständige Test-Suite |
Nächste Schritte:
Was fehlt noch?
- Transaction Decoder Tests mit echten Comdirect-JSONs (momentan nur Struktur-Tests)
- RawData speichern (für Debugging, wenn API sich ändert)
- Retry-Logic für API-Calls (Comdirect kann 429 Too Many Requests returnen)
- Token Refresh (Access-Token läuft ab nach ~10 Minuten)
Key Takeaways für Neulinge
1. Integration-Tests müssen opt-in sein
Niemals Integration-Tests by default laufen lassen:
- Kosten (API Rate-Limits, externen Datenverkehr)
- Nervfaktor (Push-TANs bei jedem Test!)
- CI/CD-Probleme (Credentials nicht verfügbar)
Lösung: Environment-Variable-Flag (RUN_INTEGRATION_TESTS=true)
2. F# Scripts sind mächtiger als du denkst
Scripts sind nicht nur für Prototyping! Nutze sie für:
- API-Exploration (neue APIs kennenlernen)
- Debugging (schneller als UI rebuilden)
- Dokumentation (zeigen, wie APIs funktionieren)
- Onboarding (neue Entwickler können explorieren)
- Interaktive Tests (z.B. Push-TAN-Bestätigung - funktioniert nicht in
dotnet test!)
Best Practice: Committe gute Scripts ins Repo!
3. Type Safety schützt nicht vor Logik-Errors
F# verhindert Type-Errors, aber nicht Semantik-Errors:
// Kompiliert, aber FALSCH:
credentials.AccountId // Als Username verwendet! 🤦
Lösung:
- Single-Case Unions für wichtige Concepts
- Gute Naming (Username vs. AccountId)
- Integration-Tests fangen semantische Bugs
Trade-off: Single-Case Unions = mehr Boilerplate. Nutze sie nur für wichtige Domain-Concepts!