@heyai-rules/pilo-masterkit 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/agents/PILO_MASTER.md +77 -77
- package/.agent/agents/architect.md +211 -211
- package/.agent/agents/backend-specialist.md +263 -263
- package/.agent/agents/build-error-resolver.md +114 -114
- package/.agent/agents/chief-of-staff.md +151 -151
- package/.agent/agents/code-archaeologist.md +106 -106
- package/.agent/agents/code-reviewer.md +237 -237
- package/.agent/agents/cpp-build-resolver.md +90 -90
- package/.agent/agents/cpp-reviewer.md +72 -72
- package/.agent/agents/csharp-reviewer.md +101 -101
- package/.agent/agents/dart-build-resolver.md +201 -201
- package/.agent/agents/database-architect.md +226 -226
- package/.agent/agents/database-reviewer.md +91 -91
- package/.agent/agents/debugger.md +225 -225
- package/.agent/agents/devops-engineer.md +242 -242
- package/.agent/agents/doc-updater.md +107 -107
- package/.agent/agents/docs-lookup.md +68 -68
- package/.agent/agents/documentation-writer.md +104 -104
- package/.agent/agents/e2e-runner.md +107 -107
- package/.agent/agents/explorer-agent.md +73 -73
- package/.agent/agents/flutter-reviewer.md +243 -243
- package/.agent/agents/frontend-specialist.md +593 -593
- package/.agent/agents/game-developer.md +162 -162
- package/.agent/agents/gan-evaluator.md +209 -209
- package/.agent/agents/gan-generator.md +131 -131
- package/.agent/agents/gan-planner.md +99 -99
- package/.agent/agents/go-build-resolver.md +94 -94
- package/.agent/agents/go-reviewer.md +76 -76
- package/.agent/agents/harness-optimizer.md +35 -35
- package/.agent/agents/healthcare-reviewer.md +83 -83
- package/.agent/agents/java-build-resolver.md +153 -153
- package/.agent/agents/java-reviewer.md +92 -92
- package/.agent/agents/kotlin-build-resolver.md +118 -118
- package/.agent/agents/kotlin-reviewer.md +159 -159
- package/.agent/agents/loop-operator.md +36 -36
- package/.agent/agents/mobile-developer.md +377 -377
- package/.agent/agents/opensource-forker.md +198 -198
- package/.agent/agents/opensource-packager.md +249 -249
- package/.agent/agents/opensource-sanitizer.md +188 -188
- package/.agent/agents/orchestrator.md +416 -416
- package/.agent/agents/penetration-tester.md +188 -188
- package/.agent/agents/performance-optimizer.md +446 -446
- package/.agent/agents/personas/athena-agent/agent.json +10 -10
- package/.agent/agents/personas/athena-agent/athena-backend-logic-architecture-profile.md +3 -3
- package/.agent/agents/personas/athena-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/athena-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/athena-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/athena-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/athena-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/athena-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/agent.json +10 -10
- package/.agent/agents/personas/da-vinci-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/da-vinci-frontend-ui-ux-design-profile.md +3 -3
- package/.agent/agents/personas/da-vinci-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/agent.json +10 -10
- package/.agent/agents/personas/duong-tang-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/tang-monk-quality-testing-documentation-profile.md +3 -3
- package/.agent/agents/personas/duong-tang-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/agent.json +10 -10
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/kongming-research-strategy-analysis-profile.md +3 -3
- package/.agent/agents/personas/gia-cat-luong-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/mihata-agent/agent.json +10 -10
- package/.agent/agents/personas/mihata-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/mihata-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/mihata-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/mihata-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/mihata-agent/mihata-multi-agent-orchestration-profile.md +3 -3
- package/.agent/agents/personas/mihata-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/mihata-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/tesla-agent/agent.json +10 -10
- package/.agent/agents/personas/tesla-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/tesla-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/tesla-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/tesla-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/tesla-agent/tesla-fullstack-system-optimization-profile.md +3 -3
- package/.agent/agents/personas/tesla-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/tesla-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/agent.json +10 -10
- package/.agent/agents/personas/tu-ma-y-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/simayi-feasibility-risk-control-profile.md +3 -3
- package/.agent/agents/personas/tu-ma-y-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/venti-agent/agent.json +10 -10
- package/.agent/agents/personas/venti-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/venti-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/venti-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/venti-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/venti-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/venti-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/venti-agent/venti-learning-communication-mentoring-profile.md +3 -3
- package/.agent/agents/planner.md +212 -212
- package/.agent/agents/product-manager.md +112 -112
- package/.agent/agents/product-owner.md +95 -95
- package/.agent/agents/project-planner.md +406 -406
- package/.agent/agents/python-reviewer.md +98 -98
- package/.agent/agents/pytorch-build-resolver.md +120 -120
- package/.agent/agents/qa-automation-engineer.md +103 -103
- package/.agent/agents/refactor-cleaner.md +85 -85
- package/.agent/agents/rust-build-resolver.md +148 -148
- package/.agent/agents/rust-reviewer.md +94 -94
- package/.agent/agents/security-auditor.md +170 -170
- package/.agent/agents/security-reviewer.md +108 -108
- package/.agent/agents/seo-specialist.md +111 -111
- package/.agent/agents/tdd-guide.md +91 -91
- package/.agent/agents/test-engineer.md +158 -158
- package/.agent/agents/typescript-reviewer.md +112 -112
- package/.agent/contexts/dev.md +20 -20
- package/.agent/contexts/research.md +26 -26
- package/.agent/contexts/review.md +22 -22
- package/.agent/hooks/hooks.json +395 -395
- package/.agent/hooks/readme.md +222 -222
- package/.agent/mcp-configs/mcp-servers.json +181 -181
- package/.agent/rules/ARCHITECTURAL_BLUEPRINTS.md +62 -62
- package/.agent/rules/CODE_CRAFTSMANSHIP.md +69 -69
- package/.agent/rules/CORE_RULES.md +72 -72
- package/.agent/rules/PROJECT_MAP.md +58 -58
- package/.agent/rules/QUALITY_ASSURANCE.md +54 -54
- package/.agent/rules/SECURITY_ARMOR.md +44 -44
- package/.agent/rules/VERSION_ORCHESTRATION.md +64 -64
- package/.agent/rules/WORKFLOW_ORCHESTRATION.md +55 -55
- package/.agent/rules/common/agents.md +50 -50
- package/.agent/rules/common/code-review.md +124 -124
- package/.agent/rules/common/coding-style.md +48 -48
- package/.agent/rules/common/development-workflow.md +44 -44
- package/.agent/rules/common/git-workflow.md +24 -24
- package/.agent/rules/common/hooks.md +30 -30
- package/.agent/rules/common/patterns.md +31 -31
- package/.agent/rules/common/performance.md +55 -55
- package/.agent/rules/common/security.md +29 -29
- package/.agent/rules/common/testing.md +29 -29
- package/.agent/rules/cpp/coding-style.md +44 -44
- package/.agent/rules/cpp/hooks.md +39 -39
- package/.agent/rules/cpp/patterns.md +51 -51
- package/.agent/rules/cpp/security.md +51 -51
- package/.agent/rules/cpp/testing.md +44 -44
- package/.agent/rules/csharp/coding-style.md +72 -72
- package/.agent/rules/csharp/hooks.md +25 -25
- package/.agent/rules/csharp/patterns.md +50 -50
- package/.agent/rules/csharp/security.md +58 -58
- package/.agent/rules/csharp/testing.md +46 -46
- package/.agent/rules/dart/coding-style.md +159 -159
- package/.agent/rules/dart/hooks.md +66 -66
- package/.agent/rules/dart/patterns.md +261 -261
- package/.agent/rules/dart/security.md +135 -135
- package/.agent/rules/dart/testing.md +215 -215
- package/.agent/rules/golang/coding-style.md +32 -32
- package/.agent/rules/golang/hooks.md +17 -17
- package/.agent/rules/golang/patterns.md +45 -45
- package/.agent/rules/golang/security.md +34 -34
- package/.agent/rules/golang/testing.md +31 -31
- package/.agent/rules/java/coding-style.md +114 -114
- package/.agent/rules/java/hooks.md +18 -18
- package/.agent/rules/java/patterns.md +146 -146
- package/.agent/rules/java/security.md +100 -100
- package/.agent/rules/java/testing.md +131 -131
- package/.agent/rules/kotlin/coding-style.md +86 -86
- package/.agent/rules/kotlin/hooks.md +17 -17
- package/.agent/rules/kotlin/patterns.md +146 -146
- package/.agent/rules/kotlin/security.md +82 -82
- package/.agent/rules/kotlin/testing.md +128 -128
- package/.agent/rules/perl/coding-style.md +46 -46
- package/.agent/rules/perl/hooks.md +22 -22
- package/.agent/rules/perl/patterns.md +76 -76
- package/.agent/rules/perl/security.md +69 -69
- package/.agent/rules/perl/testing.md +54 -54
- package/.agent/rules/php/coding-style.md +40 -40
- package/.agent/rules/php/hooks.md +24 -24
- package/.agent/rules/php/patterns.md +33 -33
- package/.agent/rules/php/security.md +37 -37
- package/.agent/rules/php/testing.md +39 -39
- package/.agent/rules/python/coding-style.md +42 -42
- package/.agent/rules/python/hooks.md +19 -19
- package/.agent/rules/python/patterns.md +39 -39
- package/.agent/rules/python/security.md +30 -30
- package/.agent/rules/python/testing.md +38 -38
- package/.agent/rules/readme.md +111 -111
- package/.agent/rules/rust/coding-style.md +151 -151
- package/.agent/rules/rust/hooks.md +16 -16
- package/.agent/rules/rust/patterns.md +168 -168
- package/.agent/rules/rust/security.md +141 -141
- package/.agent/rules/rust/testing.md +154 -154
- package/.agent/rules/swift/coding-style.md +47 -47
- package/.agent/rules/swift/hooks.md +20 -20
- package/.agent/rules/swift/patterns.md +66 -66
- package/.agent/rules/swift/security.md +33 -33
- package/.agent/rules/swift/testing.md +45 -45
- package/.agent/rules/typescript/coding-style.md +199 -199
- package/.agent/rules/typescript/hooks.md +22 -22
- package/.agent/rules/typescript/patterns.md +52 -52
- package/.agent/rules/typescript/security.md +28 -28
- package/.agent/rules/typescript/testing.md +18 -18
- package/.agent/rules/web/coding-style.md +96 -96
- package/.agent/rules/web/design-quality.md +63 -63
- package/.agent/rules/web/hooks.md +120 -120
- package/.agent/rules/web/patterns.md +79 -79
- package/.agent/rules/web/performance.md +64 -64
- package/.agent/rules/web/security.md +57 -57
- package/.agent/rules/web/testing.md +55 -55
- package/.agent/rules/zh/agents.md +50 -50
- package/.agent/rules/zh/code-review.md +124 -124
- package/.agent/rules/zh/coding-style.md +48 -48
- package/.agent/rules/zh/development-workflow.md +44 -44
- package/.agent/rules/zh/git-workflow.md +24 -24
- package/.agent/rules/zh/hooks.md +30 -30
- package/.agent/rules/zh/patterns.md +31 -31
- package/.agent/rules/zh/performance.md +55 -55
- package/.agent/rules/zh/readme.md +108 -108
- package/.agent/rules/zh/security.md +29 -29
- package/.agent/rules/zh/testing.md +29 -29
- package/.agent/scripts/auto_preview.py +148 -148
- package/.agent/scripts/checklist.py +217 -217
- package/.agent/scripts/session_manager.py +120 -120
- package/.agent/scripts/verify_all.py +327 -327
- package/.agent/skills/agent-eval/SKILL.md +145 -145
- package/.agent/skills/agent-harness-construction/SKILL.md +73 -73
- package/.agent/skills/agent-payment-x402/SKILL.md +178 -178
- package/.agent/skills/agentic-engineering/SKILL.md +63 -63
- package/.agent/skills/ai-first-engineering/SKILL.md +51 -51
- package/.agent/skills/ai-regression-testing/SKILL.md +385 -385
- package/.agent/skills/android-clean-architecture/SKILL.md +339 -339
- package/.agent/skills/api-design/SKILL.md +523 -523
- package/.agent/skills/api-patterns/SKILL.md +81 -81
- package/.agent/skills/api-patterns/api-style.md +42 -42
- package/.agent/skills/api-patterns/auth.md +24 -24
- package/.agent/skills/api-patterns/documentation.md +26 -26
- package/.agent/skills/api-patterns/graphql.md +41 -41
- package/.agent/skills/api-patterns/rate-limiting.md +31 -31
- package/.agent/skills/api-patterns/response.md +37 -37
- package/.agent/skills/api-patterns/rest.md +40 -40
- package/.agent/skills/api-patterns/scripts/api_validator.py +211 -211
- package/.agent/skills/api-patterns/security-testing.md +122 -122
- package/.agent/skills/api-patterns/trpc.md +41 -41
- package/.agent/skills/api-patterns/versioning.md +22 -22
- package/.agent/skills/app-builder/SKILL.md +75 -75
- package/.agent/skills/app-builder/agent-coordination.md +71 -71
- package/.agent/skills/app-builder/feature-building.md +53 -53
- package/.agent/skills/app-builder/project-detection.md +34 -34
- package/.agent/skills/app-builder/scaffolding.md +118 -118
- package/.agent/skills/app-builder/tech-stack.md +41 -41
- package/.agent/skills/app-builder/templates/SKILL.md +39 -39
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -76
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -92
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -88
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -88
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +83 -83
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -90
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -90
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -122
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -122
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -169
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -134
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -83
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -119
- package/.agent/skills/architecture/SKILL.md +55 -55
- package/.agent/skills/architecture/context-discovery.md +43 -43
- package/.agent/skills/architecture/examples.md +94 -94
- package/.agent/skills/architecture/pattern-selection.md +68 -68
- package/.agent/skills/architecture/patterns-reference.md +50 -50
- package/.agent/skills/architecture/trade-off-analysis.md +77 -77
- package/.agent/skills/architecture-decision-records/SKILL.md +179 -179
- package/.agent/skills/article-writing/SKILL.md +79 -79
- package/.agent/skills/autonomous-agent-harness/SKILL.md +267 -267
- package/.agent/skills/autonomous-loops/SKILL.md +610 -610
- package/.agent/skills/backend-patterns/SKILL.md +598 -598
- package/.agent/skills/bash-linux/SKILL.md +199 -199
- package/.agent/skills/behavioral-modes/SKILL.md +242 -242
- package/.agent/skills/benchmark/SKILL.md +93 -93
- package/.agent/skills/blueprint/SKILL.md +105 -105
- package/.agent/skills/brainstorming/SKILL.md +163 -163
- package/.agent/skills/brainstorming/dynamic-questioning.md +350 -350
- package/.agent/skills/brand-voice/SKILL.md +97 -97
- package/.agent/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/.agent/skills/browser-qa/SKILL.md +87 -87
- package/.agent/skills/bun-runtime/SKILL.md +84 -84
- package/.agent/skills/canary-watch/SKILL.md +99 -99
- package/.agent/skills/carrier-relationship-management/SKILL.md +212 -212
- package/.agent/skills/ck/SKILL.md +147 -147
- package/.agent/skills/ck/commands/forget.mjs +44 -44
- package/.agent/skills/ck/commands/info.mjs +24 -24
- package/.agent/skills/ck/commands/init.mjs +143 -143
- package/.agent/skills/ck/commands/list.mjs +40 -40
- package/.agent/skills/ck/commands/migrate.mjs +202 -202
- package/.agent/skills/ck/commands/resume.mjs +36 -36
- package/.agent/skills/ck/commands/save.mjs +210 -210
- package/.agent/skills/ck/commands/shared.mjs +387 -387
- package/.agent/skills/ck/hooks/session-start.mjs +224 -224
- package/.agent/skills/claude-api/SKILL.md +337 -337
- package/.agent/skills/claude-devfleet/SKILL.md +103 -103
- package/.agent/skills/clean-code/SKILL.md +201 -201
- package/.agent/skills/click-path-audit/SKILL.md +244 -244
- package/.agent/skills/clickhouse-io/SKILL.md +439 -439
- package/.agent/skills/code-review-checklist/SKILL.md +109 -109
- package/.agent/skills/codebase-onboarding/SKILL.md +233 -233
- package/.agent/skills/coding-standards/SKILL.md +530 -530
- package/.agent/skills/compose-multiplatform-patterns/SKILL.md +299 -299
- package/.agent/skills/configure-ecc/SKILL.md +367 -367
- package/.agent/skills/connections-optimizer/SKILL.md +189 -189
- package/.agent/skills/content-engine/SKILL.md +131 -131
- package/.agent/skills/content-hash-cache-pattern/SKILL.md +161 -161
- package/.agent/skills/context-budget/SKILL.md +135 -135
- package/.agent/skills/continuous-agent-loop/SKILL.md +45 -45
- package/.agent/skills/continuous-learning/SKILL.md +119 -119
- package/.agent/skills/continuous-learning/config.json +18 -18
- package/.agent/skills/continuous-learning/evaluate-session.sh +69 -69
- package/.agent/skills/continuous-learning-v2/SKILL.md +365 -365
- package/.agent/skills/continuous-learning-v2/agents/observer-loop.sh +271 -271
- package/.agent/skills/continuous-learning-v2/agents/observer.md +198 -198
- package/.agent/skills/continuous-learning-v2/agents/session-guardian.sh +150 -150
- package/.agent/skills/continuous-learning-v2/agents/start-observer.sh +244 -244
- package/.agent/skills/continuous-learning-v2/config.json +8 -8
- package/.agent/skills/continuous-learning-v2/hooks/observe.sh +428 -428
- package/.agent/skills/continuous-learning-v2/scripts/detect-project.sh +228 -228
- package/.agent/skills/continuous-learning-v2/scripts/instinct-cli.py +1426 -1426
- package/.agent/skills/continuous-learning-v2/scripts/test-parse-instinct.py +984 -984
- package/.agent/skills/cost-aware-llm-pipeline/SKILL.md +183 -183
- package/.agent/skills/cpp-coding-standards/SKILL.md +723 -723
- package/.agent/skills/cpp-testing/SKILL.md +324 -324
- package/.agent/skills/crosspost/SKILL.md +111 -111
- package/.agent/skills/csharp-testing/SKILL.md +321 -321
- package/.agent/skills/customer-billing-ops/SKILL.md +140 -140
- package/.agent/skills/customs-trade-compliance/SKILL.md +263 -263
- package/.agent/skills/dart-flutter-patterns/SKILL.md +563 -563
- package/.agent/skills/data-scraper-agent/SKILL.md +764 -764
- package/.agent/skills/database-design/SKILL.md +52 -52
- package/.agent/skills/database-design/database-selection.md +43 -43
- package/.agent/skills/database-design/indexing.md +39 -39
- package/.agent/skills/database-design/migrations.md +48 -48
- package/.agent/skills/database-design/optimization.md +36 -36
- package/.agent/skills/database-design/orm-selection.md +30 -30
- package/.agent/skills/database-design/schema-design.md +56 -56
- package/.agent/skills/database-design/scripts/schema_validator.py +172 -172
- package/.agent/skills/database-migrations/SKILL.md +429 -429
- package/.agent/skills/deep-research/SKILL.md +155 -155
- package/.agent/skills/deployment-patterns/SKILL.md +427 -427
- package/.agent/skills/deployment-procedures/SKILL.md +241 -241
- package/.agent/skills/design-system/SKILL.md +82 -82
- package/.agent/skills/django-patterns/SKILL.md +734 -734
- package/.agent/skills/django-security/SKILL.md +593 -593
- package/.agent/skills/django-tdd/SKILL.md +729 -729
- package/.agent/skills/django-verification/SKILL.md +469 -469
- package/.agent/skills/dmux-workflows/SKILL.md +191 -191
- package/.agent/skills/doc.md +177 -177
- package/.agent/skills/docker-patterns/SKILL.md +364 -364
- package/.agent/skills/documentation-lookup/SKILL.md +90 -90
- package/.agent/skills/documentation-templates/SKILL.md +194 -194
- package/.agent/skills/dotnet-patterns/SKILL.md +321 -321
- package/.agent/skills/e2e-testing/SKILL.md +326 -326
- package/.agent/skills/energy-procurement/SKILL.md +228 -228
- package/.agent/skills/enterprise-agent-ops/SKILL.md +50 -50
- package/.agent/skills/eval-harness/SKILL.md +270 -270
- package/.agent/skills/exa-search/SKILL.md +103 -103
- package/.agent/skills/fal-ai-media/SKILL.md +284 -284
- package/.agent/skills/flutter-dart-code-review/SKILL.md +435 -435
- package/.agent/skills/foundation-models-on-device/SKILL.md +243 -243
- package/.agent/skills/frontend-design/SKILL.md +452 -452
- package/.agent/skills/frontend-design/animation-guide.md +331 -331
- package/.agent/skills/frontend-design/color-system.md +311 -311
- package/.agent/skills/frontend-design/decision-trees.md +418 -418
- package/.agent/skills/frontend-design/motion-graphics.md +306 -306
- package/.agent/skills/frontend-design/scripts/accessibility_checker.py +183 -183
- package/.agent/skills/frontend-design/scripts/ux_audit.py +722 -722
- package/.agent/skills/frontend-design/typography-system.md +345 -345
- package/.agent/skills/frontend-design/ux-psychology.md +1116 -1116
- package/.agent/skills/frontend-design/visual-effects.md +383 -383
- package/.agent/skills/frontend-patterns/SKILL.md +642 -642
- package/.agent/skills/frontend-slides/SKILL.md +184 -184
- package/.agent/skills/frontend-slides/style-presets.md +330 -330
- package/.agent/skills/game-development/2d-games/SKILL.md +119 -119
- package/.agent/skills/game-development/3d-games/SKILL.md +135 -135
- package/.agent/skills/game-development/SKILL.md +167 -167
- package/.agent/skills/game-development/game-art/SKILL.md +185 -185
- package/.agent/skills/game-development/game-audio/SKILL.md +190 -190
- package/.agent/skills/game-development/game-design/SKILL.md +129 -129
- package/.agent/skills/game-development/mobile-games/SKILL.md +108 -108
- package/.agent/skills/game-development/multiplayer/SKILL.md +132 -132
- package/.agent/skills/game-development/pc-games/SKILL.md +144 -144
- package/.agent/skills/game-development/vr-ar/SKILL.md +123 -123
- package/.agent/skills/game-development/web-games/SKILL.md +150 -150
- package/.agent/skills/gan-style-harness/SKILL.md +278 -278
- package/.agent/skills/geo-fundamentals/SKILL.md +156 -156
- package/.agent/skills/geo-fundamentals/scripts/geo_checker.py +289 -289
- package/.agent/skills/git-workflow/SKILL.md +715 -715
- package/.agent/skills/golang-patterns/SKILL.md +674 -674
- package/.agent/skills/golang-testing/SKILL.md +720 -720
- package/.agent/skills/google-workspace-ops/SKILL.md +95 -95
- package/.agent/skills/healthcare-cdss-patterns/SKILL.md +245 -245
- package/.agent/skills/healthcare-emr-patterns/SKILL.md +159 -159
- package/.agent/skills/healthcare-eval-harness/SKILL.md +207 -207
- package/.agent/skills/healthcare-phi-compliance/SKILL.md +145 -145
- package/.agent/skills/hexagonal-architecture/SKILL.md +276 -276
- package/.agent/skills/i18n-localization/SKILL.md +154 -154
- package/.agent/skills/i18n-localization/scripts/i18n_checker.py +241 -241
- package/.agent/skills/intelligent-routing/SKILL.md +335 -335
- package/.agent/skills/inventory-demand-planning/SKILL.md +247 -247
- package/.agent/skills/investor-materials/SKILL.md +96 -96
- package/.agent/skills/investor-outreach/SKILL.md +91 -91
- package/.agent/skills/iterative-retrieval/SKILL.md +211 -211
- package/.agent/skills/java-coding-standards/SKILL.md +147 -147
- package/.agent/skills/jira-integration/SKILL.md +293 -293
- package/.agent/skills/jpa-patterns/SKILL.md +151 -151
- package/.agent/skills/kotlin-coroutines-flows/SKILL.md +284 -284
- package/.agent/skills/kotlin-exposed-patterns/SKILL.md +719 -719
- package/.agent/skills/kotlin-ktor-patterns/SKILL.md +689 -689
- package/.agent/skills/kotlin-patterns/SKILL.md +711 -711
- package/.agent/skills/kotlin-testing/SKILL.md +824 -824
- package/.agent/skills/laravel-patterns/SKILL.md +415 -415
- package/.agent/skills/laravel-plugin-discovery/SKILL.md +229 -229
- package/.agent/skills/laravel-security/SKILL.md +285 -285
- package/.agent/skills/laravel-tdd/SKILL.md +283 -283
- package/.agent/skills/laravel-verification/SKILL.md +179 -179
- package/.agent/skills/lead-intelligence/SKILL.md +321 -321
- package/.agent/skills/lead-intelligence/agents/enrichment-agent.md +85 -85
- package/.agent/skills/lead-intelligence/agents/mutual-mapper.md +75 -75
- package/.agent/skills/lead-intelligence/agents/outreach-drafter.md +98 -98
- package/.agent/skills/lead-intelligence/agents/signal-scorer.md +60 -60
- package/.agent/skills/lint-and-validate/SKILL.md +45 -45
- package/.agent/skills/lint-and-validate/scripts/lint_runner.py +184 -184
- package/.agent/skills/lint-and-validate/scripts/type_coverage.py +173 -173
- package/.agent/skills/liquid-glass-design/SKILL.md +279 -279
- package/.agent/skills/logistics-exception-management/SKILL.md +222 -222
- package/.agent/skills/manim-video/SKILL.md +89 -89
- package/.agent/skills/manim-video/assets/network-graph-scene.py +52 -52
- package/.agent/skills/market-research/SKILL.md +75 -75
- package/.agent/skills/mcp-server-patterns/SKILL.md +67 -67
- package/.agent/skills/mobile-design/SKILL.md +394 -394
- package/.agent/skills/mobile-design/decision-trees.md +516 -516
- package/.agent/skills/mobile-design/mobile-backend.md +491 -491
- package/.agent/skills/mobile-design/mobile-color-system.md +420 -420
- package/.agent/skills/mobile-design/mobile-debugging.md +122 -122
- package/.agent/skills/mobile-design/mobile-design-thinking.md +357 -357
- package/.agent/skills/mobile-design/mobile-navigation.md +458 -458
- package/.agent/skills/mobile-design/mobile-performance.md +767 -767
- package/.agent/skills/mobile-design/mobile-testing.md +356 -356
- package/.agent/skills/mobile-design/mobile-typography.md +433 -433
- package/.agent/skills/mobile-design/platform-android.md +666 -666
- package/.agent/skills/mobile-design/platform-ios.md +561 -561
- package/.agent/skills/mobile-design/scripts/mobile_audit.py +670 -670
- package/.agent/skills/mobile-design/touch-psychology.md +537 -537
- package/.agent/skills/nanoclaw-repl/SKILL.md +33 -33
- package/.agent/skills/nestjs-patterns/SKILL.md +230 -230
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +351 -351
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -240
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -490
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -264
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -581
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -432
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -684
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -150
- package/.agent/skills/nextjs-react-expert/9-cache-components.md +103 -103
- package/.agent/skills/nextjs-react-expert/SKILL.md +293 -293
- package/.agent/skills/nextjs-react-expert/scripts/convert_rules.py +222 -222
- package/.agent/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -252
- package/.agent/skills/nextjs-turbopack/SKILL.md +44 -44
- package/.agent/skills/nodejs-best-practices/SKILL.md +333 -333
- package/.agent/skills/nutrient-document-processing/SKILL.md +167 -167
- package/.agent/skills/nuxt4-patterns/SKILL.md +100 -100
- package/.agent/skills/openclaw-persona-forge/SKILL.md +296 -296
- package/.agent/skills/openclaw-persona-forge/gacha.py +224 -224
- package/.agent/skills/openclaw-persona-forge/gacha.sh +5 -5
- package/.agent/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/.agent/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/.agent/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/.agent/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/.agent/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/.agent/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/.agent/skills/opensource-pipeline/SKILL.md +255 -255
- package/.agent/skills/parallel-agents/SKILL.md +175 -175
- package/.agent/skills/performance-profiling/SKILL.md +143 -143
- package/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +76 -76
- package/.agent/skills/perl-patterns/SKILL.md +504 -504
- package/.agent/skills/perl-security/SKILL.md +503 -503
- package/.agent/skills/perl-testing/SKILL.md +475 -475
- package/.agent/skills/plan-writing/SKILL.md +152 -152
- package/.agent/skills/plankton-code-quality/SKILL.md +236 -236
- package/.agent/skills/postgres-patterns/SKILL.md +147 -147
- package/.agent/skills/powershell-windows/SKILL.md +167 -167
- package/.agent/skills/product-lens/SKILL.md +85 -85
- package/.agent/skills/production-scheduling/SKILL.md +238 -238
- package/.agent/skills/project-flow-ops/SKILL.md +111 -111
- package/.agent/skills/project-guidelines-example/SKILL.md +349 -349
- package/.agent/skills/prompt-optimizer/SKILL.md +397 -397
- package/.agent/skills/python-patterns/SKILL.md +750 -750
- package/.agent/skills/python-testing/SKILL.md +816 -816
- package/.agent/skills/pytorch-patterns/SKILL.md +396 -396
- package/.agent/skills/quality-nonconformance/SKILL.md +260 -260
- package/.agent/skills/ralphinho-rfc-pipeline/SKILL.md +67 -67
- package/.agent/skills/red-team-tactics/SKILL.md +199 -199
- package/.agent/skills/regex-vs-llm-structured-text/SKILL.md +220 -220
- package/.agent/skills/remotion-video-creation/SKILL.md +43 -43
- package/.agent/skills/remotion-video-creation/rules/3d.md +86 -86
- package/.agent/skills/remotion-video-creation/rules/animations.md +29 -29
- package/.agent/skills/remotion-video-creation/rules/assets/charts-bar-chart.tsx +173 -173
- package/.agent/skills/remotion-video-creation/rules/assets/text-animations-typewriter.tsx +100 -100
- package/.agent/skills/remotion-video-creation/rules/assets/text-animations-word-highlight.tsx +108 -108
- package/.agent/skills/remotion-video-creation/rules/assets.md +78 -78
- package/.agent/skills/remotion-video-creation/rules/audio.md +172 -172
- package/.agent/skills/remotion-video-creation/rules/calculate-metadata.md +104 -104
- package/.agent/skills/remotion-video-creation/rules/can-decode.md +75 -75
- package/.agent/skills/remotion-video-creation/rules/charts.md +58 -58
- package/.agent/skills/remotion-video-creation/rules/compositions.md +146 -146
- package/.agent/skills/remotion-video-creation/rules/display-captions.md +126 -126
- package/.agent/skills/remotion-video-creation/rules/extract-frames.md +229 -229
- package/.agent/skills/remotion-video-creation/rules/fonts.md +152 -152
- package/.agent/skills/remotion-video-creation/rules/get-audio-duration.md +58 -58
- package/.agent/skills/remotion-video-creation/rules/get-video-dimensions.md +68 -68
- package/.agent/skills/remotion-video-creation/rules/get-video-duration.md +58 -58
- package/.agent/skills/remotion-video-creation/rules/gifs.md +138 -138
- package/.agent/skills/remotion-video-creation/rules/images.md +130 -130
- package/.agent/skills/remotion-video-creation/rules/import-srt-captions.md +67 -67
- package/.agent/skills/remotion-video-creation/rules/lottie.md +67 -67
- package/.agent/skills/remotion-video-creation/rules/measuring-dom-nodes.md +34 -34
- package/.agent/skills/remotion-video-creation/rules/measuring-text.md +143 -143
- package/.agent/skills/remotion-video-creation/rules/sequencing.md +106 -106
- package/.agent/skills/remotion-video-creation/rules/tailwind.md +11 -11
- package/.agent/skills/remotion-video-creation/rules/text-animations.md +20 -20
- package/.agent/skills/remotion-video-creation/rules/timing.md +179 -179
- package/.agent/skills/remotion-video-creation/rules/transcribe-captions.md +19 -19
- package/.agent/skills/remotion-video-creation/rules/transitions.md +122 -122
- package/.agent/skills/remotion-video-creation/rules/trimming.md +52 -52
- package/.agent/skills/remotion-video-creation/rules/videos.md +171 -171
- package/.agent/skills/repo-scan/SKILL.md +63 -63
- package/.agent/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/.agent/skills/rules-distill/SKILL.md +264 -264
- package/.agent/skills/rules-distill/scripts/scan-rules.sh +58 -58
- package/.agent/skills/rules-distill/scripts/scan-skills.sh +129 -129
- package/.agent/skills/rust-patterns/SKILL.md +499 -499
- package/.agent/skills/rust-pro/SKILL.md +175 -175
- package/.agent/skills/rust-testing/SKILL.md +500 -500
- package/.agent/skills/safety-guard/SKILL.md +75 -75
- package/.agent/skills/santa-method/SKILL.md +306 -306
- package/.agent/skills/search-first/SKILL.md +161 -161
- package/.agent/skills/security-review/SKILL.md +495 -495
- package/.agent/skills/security-review/cloud-infrastructure-security.md +361 -361
- package/.agent/skills/security-scan/SKILL.md +165 -165
- package/.agent/skills/seo-fundamentals/SKILL.md +129 -129
- package/.agent/skills/seo-fundamentals/scripts/seo_checker.py +219 -219
- package/.agent/skills/server-management/SKILL.md +161 -161
- package/.agent/skills/skill-comply/SKILL.md +58 -58
- package/.agent/skills/skill-comply/fixtures/compliant-trace.jsonl +5 -5
- package/.agent/skills/skill-comply/fixtures/noncompliant-trace.jsonl +3 -3
- package/.agent/skills/skill-comply/fixtures/tdd-spec.yaml +44 -44
- package/.agent/skills/skill-comply/prompts/classifier.md +24 -24
- package/.agent/skills/skill-comply/prompts/scenario-generator.md +62 -62
- package/.agent/skills/skill-comply/prompts/spec-generator.md +42 -42
- package/.agent/skills/skill-comply/pyproject.toml +15 -15
- package/.agent/skills/skill-comply/scripts/classifier.py +85 -85
- package/.agent/skills/skill-comply/scripts/grader.py +122 -122
- package/.agent/skills/skill-comply/scripts/parser.py +107 -107
- package/.agent/skills/skill-comply/scripts/report.py +170 -170
- package/.agent/skills/skill-comply/scripts/run.py +127 -127
- package/.agent/skills/skill-comply/scripts/runner.py +161 -161
- package/.agent/skills/skill-comply/scripts/scenario-generator.py +70 -70
- package/.agent/skills/skill-comply/scripts/spec-generator.py +72 -72
- package/.agent/skills/skill-comply/scripts/utils.py +13 -13
- package/.agent/skills/skill-comply/tests/test-grader.py +137 -137
- package/.agent/skills/skill-comply/tests/test-parser.py +90 -90
- package/.agent/skills/skill-stocktake/SKILL.md +193 -193
- package/.agent/skills/skill-stocktake/scripts/quick-diff.sh +87 -87
- package/.agent/skills/skill-stocktake/scripts/save-results.sh +56 -56
- package/.agent/skills/skill-stocktake/scripts/scan.sh +170 -170
- package/.agent/skills/social-graph-ranker/SKILL.md +154 -154
- package/.agent/skills/springboot-patterns/SKILL.md +314 -314
- package/.agent/skills/springboot-security/SKILL.md +272 -272
- package/.agent/skills/springboot-tdd/SKILL.md +158 -158
- package/.agent/skills/springboot-verification/SKILL.md +231 -231
- package/.agent/skills/strategic-compact/SKILL.md +131 -131
- package/.agent/skills/strategic-compact/suggest-compact.sh +54 -54
- package/.agent/skills/swift-actor-persistence/SKILL.md +143 -143
- package/.agent/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/.agent/skills/swift-protocol-di-testing/SKILL.md +190 -190
- package/.agent/skills/swiftui-patterns/SKILL.md +259 -259
- package/.agent/skills/systematic-debugging/SKILL.md +109 -109
- package/.agent/skills/tailwind-patterns/SKILL.md +269 -269
- package/.agent/skills/tdd-workflow/SKILL.md +463 -463
- package/.agent/skills/team-builder/SKILL.md +168 -168
- package/.agent/skills/testing-patterns/SKILL.md +178 -178
- package/.agent/skills/testing-patterns/scripts/test_runner.py +219 -219
- package/.agent/skills/token-budget-advisor/SKILL.md +133 -133
- package/.agent/skills/ui-demo/SKILL.md +465 -465
- package/.agent/skills/ui-ux-pro-max/SKILL.md +292 -292
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -26
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -97
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -101
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -31
- package/.agent/skills/ui-ux-pro-max/data/products.csv +96 -96
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -68
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +57 -57
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -253
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -114
- package/.agent/skills/verification-loop/SKILL.md +126 -126
- package/.agent/skills/video-editing/SKILL.md +310 -310
- package/.agent/skills/videodb/SKILL.md +374 -374
- package/.agent/skills/videodb/reference/api-reference.md +550 -550
- package/.agent/skills/videodb/reference/capture-reference.md +407 -407
- package/.agent/skills/videodb/reference/capture.md +101 -101
- package/.agent/skills/videodb/reference/editor.md +443 -443
- package/.agent/skills/videodb/reference/generative.md +331 -331
- package/.agent/skills/videodb/reference/rtstream-reference.md +564 -564
- package/.agent/skills/videodb/reference/rtstream.md +65 -65
- package/.agent/skills/videodb/reference/search.md +230 -230
- package/.agent/skills/videodb/reference/streaming.md +406 -406
- package/.agent/skills/videodb/reference/use-cases.md +118 -118
- package/.agent/skills/videodb/scripts/ws-listener.py +282 -282
- package/.agent/skills/visa-doc-translate/SKILL.md +117 -117
- package/.agent/skills/visa-doc-translate/readme.md +86 -86
- package/.agent/skills/vulnerability-scanner/SKILL.md +276 -276
- package/.agent/skills/vulnerability-scanner/checklists.md +121 -121
- package/.agent/skills/vulnerability-scanner/scripts/security_scan.py +458 -458
- package/.agent/skills/web-design-guidelines/SKILL.md +57 -57
- package/.agent/skills/webapp-testing/SKILL.md +187 -187
- package/.agent/skills/webapp-testing/scripts/playwright_runner.py +173 -173
- package/.agent/skills/workspace-surface-audit/SKILL.md +125 -125
- package/.agent/skills/x-api/SKILL.md +230 -230
- package/.agent/tasks/lessons.md +40 -40
- package/.agent/tasks/todo.md +33 -33
- package/.agent/tasks/two-track-merge-contract.md +1 -1
- package/.agent/workflows/aside.md +164 -164
- package/.agent/workflows/brainstorm.md +113 -113
- package/.agent/workflows/build-fix.md +62 -62
- package/.agent/workflows/checkpoint.md +74 -74
- package/.agent/workflows/claw.md +23 -23
- package/.agent/workflows/clean-memory.md +34 -34
- package/.agent/workflows/code-review.md +289 -289
- package/.agent/workflows/context-budget.md +23 -23
- package/.agent/workflows/cpp-build.md +173 -173
- package/.agent/workflows/cpp-review.md +132 -132
- package/.agent/workflows/cpp-test.md +251 -251
- package/.agent/workflows/create.md +59 -59
- package/.agent/workflows/debug.md +103 -103
- package/.agent/workflows/deploy.md +176 -176
- package/.agent/workflows/devfleet.md +23 -23
- package/.agent/workflows/docs.md +23 -23
- package/.agent/workflows/e2e.md +268 -268
- package/.agent/workflows/enhance.md +63 -63
- package/.agent/workflows/eval.md +23 -23
- package/.agent/workflows/evolve.md +178 -178
- package/.agent/workflows/flutter-build.md +164 -164
- package/.agent/workflows/flutter-review.md +116 -116
- package/.agent/workflows/flutter-test.md +144 -144
- package/.agent/workflows/gan-build.md +99 -99
- package/.agent/workflows/gan-design.md +35 -35
- package/.agent/workflows/go-build.md +183 -183
- package/.agent/workflows/go-review.md +148 -148
- package/.agent/workflows/go-test.md +268 -268
- package/.agent/workflows/gradle-build.md +70 -70
- package/.agent/workflows/harness-audit.md +73 -73
- package/.agent/workflows/init-docs.md +46 -46
- package/.agent/workflows/instinct-export.md +66 -66
- package/.agent/workflows/instinct-import.md +114 -114
- package/.agent/workflows/instinct-status.md +59 -59
- package/.agent/workflows/jira.md +106 -106
- package/.agent/workflows/kotlin-build.md +174 -174
- package/.agent/workflows/kotlin-review.md +140 -140
- package/.agent/workflows/kotlin-test.md +312 -312
- package/.agent/workflows/learn-eval.md +116 -116
- package/.agent/workflows/learn.md +70 -70
- package/.agent/workflows/loop-start.md +32 -32
- package/.agent/workflows/loop-status.md +24 -24
- package/.agent/workflows/model-route.md +26 -26
- package/.agent/workflows/multi-backend.md +158 -158
- package/.agent/workflows/multi-execute.md +315 -315
- package/.agent/workflows/multi-frontend.md +158 -158
- package/.agent/workflows/multi-plan.md +268 -268
- package/.agent/workflows/multi-workflow.md +191 -191
- package/.agent/workflows/orchestrate.md +135 -135
- package/.agent/workflows/plan.md +117 -117
- package/.agent/workflows/pm2.md +272 -272
- package/.agent/workflows/preview.md +81 -81
- package/.agent/workflows/projects.md +39 -39
- package/.agent/workflows/promote.md +41 -41
- package/.agent/workflows/prompt-optimize.md +23 -23
- package/.agent/workflows/prp-commit.md +112 -112
- package/.agent/workflows/prp-implement.md +385 -385
- package/.agent/workflows/prp-plan.md +502 -502
- package/.agent/workflows/prp-pr.md +184 -184
- package/.agent/workflows/prp-prd.md +447 -447
- package/.agent/workflows/prune.md +31 -31
- package/.agent/workflows/python-review.md +297 -297
- package/.agent/workflows/quality-gate.md +29 -29
- package/.agent/workflows/refactor-clean.md +80 -80
- package/.agent/workflows/resume-session.md +156 -156
- package/.agent/workflows/rules-distill.md +20 -20
- package/.agent/workflows/rust-build.md +187 -187
- package/.agent/workflows/rust-review.md +142 -142
- package/.agent/workflows/rust-test.md +308 -308
- package/.agent/workflows/santa-loop.md +175 -175
- package/.agent/workflows/save-session.md +275 -275
- package/.agent/workflows/sessions.md +333 -333
- package/.agent/workflows/setup-pm.md +80 -80
- package/.agent/workflows/skill-create.md +174 -174
- package/.agent/workflows/skill-health.md +54 -54
- package/.agent/workflows/status.md +86 -86
- package/.agent/workflows/tdd.md +231 -231
- package/.agent/workflows/test-coverage.md +69 -69
- package/.agent/workflows/test.md +144 -144
- package/.agent/workflows/ui-ux-pro-max.md +295 -295
- package/.agent/workflows/update-codemaps.md +72 -72
- package/.agent/workflows/update-docs.md +84 -84
- package/.agent/workflows/verify.md +23 -23
- package/LICENSE +176 -176
- package/README.md +144 -144
- package/package.json +1 -1
- package/scripts/release-check.js +55 -55
- package/src/bin/cli.js +424 -354
- package/src/lib/installer.js +223 -11
|
@@ -1,824 +1,824 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: kotlin-testing
|
|
3
|
-
description: Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.
|
|
4
|
-
origin: ECC
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Kotlin Testing Patterns
|
|
8
|
-
|
|
9
|
-
Comprehensive Kotlin testing patterns for writing reliable, maintainable tests following TDD methodology with Kotest and MockK.
|
|
10
|
-
|
|
11
|
-
## When to Use
|
|
12
|
-
|
|
13
|
-
- Writing new Kotlin functions or classes
|
|
14
|
-
- Adding test coverage to existing Kotlin code
|
|
15
|
-
- Implementing property-based tests
|
|
16
|
-
- Following TDD workflow in Kotlin projects
|
|
17
|
-
- Configuring Kover for code coverage
|
|
18
|
-
|
|
19
|
-
## How It Works
|
|
20
|
-
|
|
21
|
-
1. **Identify target code** — Find the function, class, or module to test
|
|
22
|
-
2. **Write a Kotest spec** — Choose a spec style (StringSpec, FunSpec, BehaviorSpec) matching the test scope
|
|
23
|
-
3. **Mock dependencies** — Use MockK to isolate the unit under test
|
|
24
|
-
4. **Run tests (RED)** — Verify the test fails with the expected error
|
|
25
|
-
5. **Implement code (GREEN)** — Write minimal code to pass the test
|
|
26
|
-
6. **Refactor** — Improve the implementation while keeping tests green
|
|
27
|
-
7. **Check coverage** — Run `./gradlew koverHtmlReport` and verify 80%+ coverage
|
|
28
|
-
|
|
29
|
-
## Examples
|
|
30
|
-
|
|
31
|
-
The following sections contain detailed, runnable examples for each testing pattern:
|
|
32
|
-
|
|
33
|
-
### Quick Reference
|
|
34
|
-
|
|
35
|
-
- **Kotest specs** — StringSpec, FunSpec, BehaviorSpec, DescribeSpec examples in [Kotest Spec Styles](#kotest-spec-styles)
|
|
36
|
-
- **Mocking** — MockK setup, coroutine mocking, argument capture in [MockK](#mockk)
|
|
37
|
-
- **TDD walkthrough** — Full RED/GREEN/REFACTOR cycle with EmailValidator in [TDD Workflow for Kotlin](#tdd-workflow-for-kotlin)
|
|
38
|
-
- **Coverage** — Kover configuration and commands in [Kover Coverage](#kover-coverage)
|
|
39
|
-
- **Ktor testing** — testApplication setup in [Ktor testApplication Testing](#ktor-testapplication-testing)
|
|
40
|
-
|
|
41
|
-
### TDD Workflow for Kotlin
|
|
42
|
-
|
|
43
|
-
#### The RED-GREEN-REFACTOR Cycle
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
RED -> Write a failing test first
|
|
47
|
-
GREEN -> Write minimal code to pass the test
|
|
48
|
-
REFACTOR -> Improve code while keeping tests green
|
|
49
|
-
REPEAT -> Continue with next requirement
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
#### Step-by-Step TDD in Kotlin
|
|
53
|
-
|
|
54
|
-
```kotlin
|
|
55
|
-
// Step 1: Define the interface/signature
|
|
56
|
-
// EmailValidator.kt
|
|
57
|
-
package com.example.validator
|
|
58
|
-
|
|
59
|
-
fun validateEmail(email: String): Result<String> {
|
|
60
|
-
TODO("not implemented")
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Step 2: Write failing test (RED)
|
|
64
|
-
// EmailValidatorTest.kt
|
|
65
|
-
package com.example.validator
|
|
66
|
-
|
|
67
|
-
import io.kotest.core.spec.style.StringSpec
|
|
68
|
-
import io.kotest.matchers.result.shouldBeFailure
|
|
69
|
-
import io.kotest.matchers.result.shouldBeSuccess
|
|
70
|
-
|
|
71
|
-
class EmailValidatorTest : StringSpec({
|
|
72
|
-
"valid email returns success" {
|
|
73
|
-
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
"empty email returns failure" {
|
|
77
|
-
validateEmail("").shouldBeFailure()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
"email without @ returns failure" {
|
|
81
|
-
validateEmail("userexample.com").shouldBeFailure()
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// Step 3: Run tests - verify FAIL
|
|
86
|
-
// $ ./gradlew test
|
|
87
|
-
// EmailValidatorTest > valid email returns success FAILED
|
|
88
|
-
// kotlin.NotImplementedError: An operation is not implemented
|
|
89
|
-
|
|
90
|
-
// Step 4: Implement minimal code (GREEN)
|
|
91
|
-
fun validateEmail(email: String): Result<String> {
|
|
92
|
-
if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
|
|
93
|
-
if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
|
|
94
|
-
val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
|
95
|
-
if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
|
|
96
|
-
return Result.success(email)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Step 5: Run tests - verify PASS
|
|
100
|
-
// $ ./gradlew test
|
|
101
|
-
// EmailValidatorTest > valid email returns success PASSED
|
|
102
|
-
// EmailValidatorTest > empty email returns failure PASSED
|
|
103
|
-
// EmailValidatorTest > email without @ returns failure PASSED
|
|
104
|
-
|
|
105
|
-
// Step 6: Refactor if needed, verify tests still pass
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Kotest Spec Styles
|
|
109
|
-
|
|
110
|
-
#### StringSpec (Simplest)
|
|
111
|
-
|
|
112
|
-
```kotlin
|
|
113
|
-
class CalculatorTest : StringSpec({
|
|
114
|
-
"add two positive numbers" {
|
|
115
|
-
Calculator.add(2, 3) shouldBe 5
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
"add negative numbers" {
|
|
119
|
-
Calculator.add(-1, -2) shouldBe -3
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
"add zero" {
|
|
123
|
-
Calculator.add(0, 5) shouldBe 5
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
#### FunSpec (JUnit-like)
|
|
129
|
-
|
|
130
|
-
```kotlin
|
|
131
|
-
class UserServiceTest : FunSpec({
|
|
132
|
-
val repository = mockk<UserRepository>()
|
|
133
|
-
val service = UserService(repository)
|
|
134
|
-
|
|
135
|
-
test("getUser returns user when found") {
|
|
136
|
-
val expected = User(id = "1", name = "Alice")
|
|
137
|
-
coEvery { repository.findById("1") } returns expected
|
|
138
|
-
|
|
139
|
-
val result = service.getUser("1")
|
|
140
|
-
|
|
141
|
-
result shouldBe expected
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
test("getUser throws when not found") {
|
|
145
|
-
coEvery { repository.findById("999") } returns null
|
|
146
|
-
|
|
147
|
-
shouldThrow<UserNotFoundException> {
|
|
148
|
-
service.getUser("999")
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
#### BehaviorSpec (BDD Style)
|
|
155
|
-
|
|
156
|
-
```kotlin
|
|
157
|
-
class OrderServiceTest : BehaviorSpec({
|
|
158
|
-
val repository = mockk<OrderRepository>()
|
|
159
|
-
val paymentService = mockk<PaymentService>()
|
|
160
|
-
val service = OrderService(repository, paymentService)
|
|
161
|
-
|
|
162
|
-
Given("a valid order request") {
|
|
163
|
-
val request = CreateOrderRequest(
|
|
164
|
-
userId = "user-1",
|
|
165
|
-
items = listOf(OrderItem("product-1", quantity = 2)),
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
When("the order is placed") {
|
|
169
|
-
coEvery { paymentService.charge(any()) } returns PaymentResult.Success
|
|
170
|
-
coEvery { repository.save(any()) } answers { firstArg() }
|
|
171
|
-
|
|
172
|
-
val result = service.placeOrder(request)
|
|
173
|
-
|
|
174
|
-
Then("it should return a confirmed order") {
|
|
175
|
-
result.status shouldBe OrderStatus.CONFIRMED
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
Then("it should charge payment") {
|
|
179
|
-
coVerify(exactly = 1) { paymentService.charge(any()) }
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
When("payment fails") {
|
|
184
|
-
coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
|
|
185
|
-
|
|
186
|
-
Then("it should throw PaymentException") {
|
|
187
|
-
shouldThrow<PaymentException> {
|
|
188
|
-
service.placeOrder(request)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
#### DescribeSpec (RSpec Style)
|
|
197
|
-
|
|
198
|
-
```kotlin
|
|
199
|
-
class UserValidatorTest : DescribeSpec({
|
|
200
|
-
describe("validateUser") {
|
|
201
|
-
val validator = UserValidator()
|
|
202
|
-
|
|
203
|
-
context("with valid input") {
|
|
204
|
-
it("accepts a normal user") {
|
|
205
|
-
val user = CreateUserRequest("Alice", "alice@example.com")
|
|
206
|
-
validator.validate(user).shouldBeValid()
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
context("with invalid name") {
|
|
211
|
-
it("rejects blank name") {
|
|
212
|
-
val user = CreateUserRequest("", "alice@example.com")
|
|
213
|
-
validator.validate(user).shouldBeInvalid()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
it("rejects name exceeding max length") {
|
|
217
|
-
val user = CreateUserRequest("A".repeat(256), "alice@example.com")
|
|
218
|
-
validator.validate(user).shouldBeInvalid()
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Kotest Matchers
|
|
226
|
-
|
|
227
|
-
#### Core Matchers
|
|
228
|
-
|
|
229
|
-
```kotlin
|
|
230
|
-
import io.kotest.matchers.shouldBe
|
|
231
|
-
import io.kotest.matchers.shouldNotBe
|
|
232
|
-
import io.kotest.matchers.string.*
|
|
233
|
-
import io.kotest.matchers.collections.*
|
|
234
|
-
import io.kotest.matchers.nulls.*
|
|
235
|
-
|
|
236
|
-
// Equality
|
|
237
|
-
result shouldBe expected
|
|
238
|
-
result shouldNotBe unexpected
|
|
239
|
-
|
|
240
|
-
// Strings
|
|
241
|
-
name shouldStartWith "Al"
|
|
242
|
-
name shouldEndWith "ice"
|
|
243
|
-
name shouldContain "lic"
|
|
244
|
-
name shouldMatch Regex("[A-Z][a-z]+")
|
|
245
|
-
name.shouldBeBlank()
|
|
246
|
-
|
|
247
|
-
// Collections
|
|
248
|
-
list shouldContain "item"
|
|
249
|
-
list shouldHaveSize 3
|
|
250
|
-
list.shouldBeSorted()
|
|
251
|
-
list.shouldContainAll("a", "b", "c")
|
|
252
|
-
list.shouldBeEmpty()
|
|
253
|
-
|
|
254
|
-
// Nulls
|
|
255
|
-
result.shouldNotBeNull()
|
|
256
|
-
result.shouldBeNull()
|
|
257
|
-
|
|
258
|
-
// Types
|
|
259
|
-
result.shouldBeInstanceOf<User>()
|
|
260
|
-
|
|
261
|
-
// Numbers
|
|
262
|
-
count shouldBeGreaterThan 0
|
|
263
|
-
price shouldBeInRange 1.0..100.0
|
|
264
|
-
|
|
265
|
-
// Exceptions
|
|
266
|
-
shouldThrow<IllegalArgumentException> {
|
|
267
|
-
validateAge(-1)
|
|
268
|
-
}.message shouldBe "Age must be positive"
|
|
269
|
-
|
|
270
|
-
shouldNotThrow<Exception> {
|
|
271
|
-
validateAge(25)
|
|
272
|
-
}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
#### Custom Matchers
|
|
276
|
-
|
|
277
|
-
```kotlin
|
|
278
|
-
fun beActiveUser() = object : Matcher<User> {
|
|
279
|
-
override fun test(value: User) = MatcherResult(
|
|
280
|
-
value.isActive && value.lastLogin != null,
|
|
281
|
-
{ "User ${value.id} should be active with a last login" },
|
|
282
|
-
{ "User ${value.id} should not be active" },
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Usage
|
|
287
|
-
user should beActiveUser()
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### MockK
|
|
291
|
-
|
|
292
|
-
#### Basic Mocking
|
|
293
|
-
|
|
294
|
-
```kotlin
|
|
295
|
-
class UserServiceTest : FunSpec({
|
|
296
|
-
val repository = mockk<UserRepository>()
|
|
297
|
-
val logger = mockk<Logger>(relaxed = true) // Relaxed: returns defaults
|
|
298
|
-
val service = UserService(repository, logger)
|
|
299
|
-
|
|
300
|
-
beforeTest {
|
|
301
|
-
clearMocks(repository, logger)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
test("findUser delegates to repository") {
|
|
305
|
-
val expected = User(id = "1", name = "Alice")
|
|
306
|
-
every { repository.findById("1") } returns expected
|
|
307
|
-
|
|
308
|
-
val result = service.findUser("1")
|
|
309
|
-
|
|
310
|
-
result shouldBe expected
|
|
311
|
-
verify(exactly = 1) { repository.findById("1") }
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
test("findUser returns null for unknown id") {
|
|
315
|
-
every { repository.findById(any()) } returns null
|
|
316
|
-
|
|
317
|
-
val result = service.findUser("unknown")
|
|
318
|
-
|
|
319
|
-
result.shouldBeNull()
|
|
320
|
-
}
|
|
321
|
-
})
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
#### Coroutine Mocking
|
|
325
|
-
|
|
326
|
-
```kotlin
|
|
327
|
-
class AsyncUserServiceTest : FunSpec({
|
|
328
|
-
val repository = mockk<UserRepository>()
|
|
329
|
-
val service = UserService(repository)
|
|
330
|
-
|
|
331
|
-
test("getUser suspending function") {
|
|
332
|
-
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
|
|
333
|
-
|
|
334
|
-
val result = service.getUser("1")
|
|
335
|
-
|
|
336
|
-
result.name shouldBe "Alice"
|
|
337
|
-
coVerify { repository.findById("1") }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
test("getUser with delay") {
|
|
341
|
-
coEvery { repository.findById("1") } coAnswers {
|
|
342
|
-
delay(100) // Simulate async work
|
|
343
|
-
User(id = "1", name = "Alice")
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
val result = service.getUser("1")
|
|
347
|
-
result.name shouldBe "Alice"
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
#### Argument Capture
|
|
353
|
-
|
|
354
|
-
```kotlin
|
|
355
|
-
test("save captures the user argument") {
|
|
356
|
-
val slot = slot<User>()
|
|
357
|
-
coEvery { repository.save(capture(slot)) } returns Unit
|
|
358
|
-
|
|
359
|
-
service.createUser(CreateUserRequest("Alice", "alice@example.com"))
|
|
360
|
-
|
|
361
|
-
slot.captured.name shouldBe "Alice"
|
|
362
|
-
slot.captured.email shouldBe "alice@example.com"
|
|
363
|
-
slot.captured.id.shouldNotBeNull()
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
#### Spy and Partial Mocking
|
|
368
|
-
|
|
369
|
-
```kotlin
|
|
370
|
-
test("spy on real object") {
|
|
371
|
-
val realService = UserService(repository)
|
|
372
|
-
val spy = spyk(realService)
|
|
373
|
-
|
|
374
|
-
every { spy.generateId() } returns "fixed-id"
|
|
375
|
-
|
|
376
|
-
spy.createUser(request)
|
|
377
|
-
|
|
378
|
-
verify { spy.generateId() } // Overridden
|
|
379
|
-
// Other methods use real implementation
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Coroutine Testing
|
|
384
|
-
|
|
385
|
-
#### runTest for Suspend Functions
|
|
386
|
-
|
|
387
|
-
```kotlin
|
|
388
|
-
import kotlinx.coroutines.test.runTest
|
|
389
|
-
|
|
390
|
-
class CoroutineServiceTest : FunSpec({
|
|
391
|
-
test("concurrent fetches complete together") {
|
|
392
|
-
runTest {
|
|
393
|
-
val service = DataService(testScope = this)
|
|
394
|
-
|
|
395
|
-
val result = service.fetchAllData()
|
|
396
|
-
|
|
397
|
-
result.users.shouldNotBeEmpty()
|
|
398
|
-
result.products.shouldNotBeEmpty()
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
test("timeout after delay") {
|
|
403
|
-
runTest {
|
|
404
|
-
val service = SlowService()
|
|
405
|
-
|
|
406
|
-
shouldThrow<TimeoutCancellationException> {
|
|
407
|
-
withTimeout(100) {
|
|
408
|
-
service.slowOperation() // Takes > 100ms
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
})
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
#### Testing Flows
|
|
417
|
-
|
|
418
|
-
```kotlin
|
|
419
|
-
import io.kotest.matchers.collections.shouldContainInOrder
|
|
420
|
-
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
421
|
-
import kotlinx.coroutines.flow.toList
|
|
422
|
-
import kotlinx.coroutines.launch
|
|
423
|
-
import kotlinx.coroutines.test.advanceTimeBy
|
|
424
|
-
import kotlinx.coroutines.test.runTest
|
|
425
|
-
|
|
426
|
-
class FlowServiceTest : FunSpec({
|
|
427
|
-
test("observeUsers emits updates") {
|
|
428
|
-
runTest {
|
|
429
|
-
val service = UserFlowService()
|
|
430
|
-
|
|
431
|
-
val emissions = service.observeUsers()
|
|
432
|
-
.take(3)
|
|
433
|
-
.toList()
|
|
434
|
-
|
|
435
|
-
emissions shouldHaveSize 3
|
|
436
|
-
emissions.last().shouldNotBeEmpty()
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
test("searchUsers debounces input") {
|
|
441
|
-
runTest {
|
|
442
|
-
val service = SearchService()
|
|
443
|
-
val queries = MutableSharedFlow<String>()
|
|
444
|
-
|
|
445
|
-
val results = mutableListOf<List<User>>()
|
|
446
|
-
val job = launch {
|
|
447
|
-
service.searchUsers(queries).collect { results.add(it) }
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
queries.emit("a")
|
|
451
|
-
queries.emit("ab")
|
|
452
|
-
queries.emit("abc") // Only this should trigger search
|
|
453
|
-
advanceTimeBy(500)
|
|
454
|
-
|
|
455
|
-
results shouldHaveSize 1
|
|
456
|
-
job.cancel()
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
})
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
#### TestDispatcher
|
|
463
|
-
|
|
464
|
-
```kotlin
|
|
465
|
-
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
466
|
-
import kotlinx.coroutines.test.advanceUntilIdle
|
|
467
|
-
|
|
468
|
-
class DispatcherTest : FunSpec({
|
|
469
|
-
test("uses test dispatcher for controlled execution") {
|
|
470
|
-
val dispatcher = StandardTestDispatcher()
|
|
471
|
-
|
|
472
|
-
runTest(dispatcher) {
|
|
473
|
-
var completed = false
|
|
474
|
-
|
|
475
|
-
launch {
|
|
476
|
-
delay(1000)
|
|
477
|
-
completed = true
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
completed shouldBe false
|
|
481
|
-
advanceTimeBy(1000)
|
|
482
|
-
completed shouldBe true
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
})
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### Property-Based Testing
|
|
489
|
-
|
|
490
|
-
#### Kotest Property Testing
|
|
491
|
-
|
|
492
|
-
```kotlin
|
|
493
|
-
import io.kotest.core.spec.style.FunSpec
|
|
494
|
-
import io.kotest.property.Arb
|
|
495
|
-
import io.kotest.property.arbitrary.*
|
|
496
|
-
import io.kotest.property.forAll
|
|
497
|
-
import io.kotest.property.checkAll
|
|
498
|
-
import kotlinx.serialization.json.Json
|
|
499
|
-
import kotlinx.serialization.encodeToString
|
|
500
|
-
import kotlinx.serialization.decodeFromString
|
|
501
|
-
|
|
502
|
-
// Note: The serialization roundtrip test below requires the User data class
|
|
503
|
-
// to be annotated with @Serializable (from kotlinx.serialization).
|
|
504
|
-
|
|
505
|
-
class PropertyTest : FunSpec({
|
|
506
|
-
test("string reverse is involutory") {
|
|
507
|
-
forAll<String> { s ->
|
|
508
|
-
s.reversed().reversed() == s
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
test("list sort is idempotent") {
|
|
513
|
-
forAll(Arb.list(Arb.int())) { list ->
|
|
514
|
-
list.sorted() == list.sorted().sorted()
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
test("serialization roundtrip preserves data") {
|
|
519
|
-
checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
|
|
520
|
-
User(name = name, email = "$email@test.com")
|
|
521
|
-
}) { user ->
|
|
522
|
-
val json = Json.encodeToString(user)
|
|
523
|
-
val decoded = Json.decodeFromString<User>(json)
|
|
524
|
-
decoded shouldBe user
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
})
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
#### Custom Generators
|
|
531
|
-
|
|
532
|
-
```kotlin
|
|
533
|
-
val userArb: Arb<User> = Arb.bind(
|
|
534
|
-
Arb.string(minSize = 1, maxSize = 50),
|
|
535
|
-
Arb.email(),
|
|
536
|
-
Arb.enum<Role>(),
|
|
537
|
-
) { name, email, role ->
|
|
538
|
-
User(
|
|
539
|
-
id = UserId(UUID.randomUUID().toString()),
|
|
540
|
-
name = name,
|
|
541
|
-
email = Email(email),
|
|
542
|
-
role = role,
|
|
543
|
-
)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
val moneyArb: Arb<Money> = Arb.bind(
|
|
547
|
-
Arb.long(1L..1_000_000L),
|
|
548
|
-
Arb.enum<Currency>(),
|
|
549
|
-
) { amount, currency ->
|
|
550
|
-
Money(amount, currency)
|
|
551
|
-
}
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
### Data-Driven Testing
|
|
555
|
-
|
|
556
|
-
#### withData in Kotest
|
|
557
|
-
|
|
558
|
-
```kotlin
|
|
559
|
-
class ParserTest : FunSpec({
|
|
560
|
-
context("parsing valid dates") {
|
|
561
|
-
withData(
|
|
562
|
-
"2026-01-15" to LocalDate(2026, 1, 15),
|
|
563
|
-
"2026-12-31" to LocalDate(2026, 12, 31),
|
|
564
|
-
"2000-01-01" to LocalDate(2000, 1, 1),
|
|
565
|
-
) { (input, expected) ->
|
|
566
|
-
parseDate(input) shouldBe expected
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
context("rejecting invalid dates") {
|
|
571
|
-
withData(
|
|
572
|
-
nameFn = { "rejects '$it'" },
|
|
573
|
-
"not-a-date",
|
|
574
|
-
"2026-13-01",
|
|
575
|
-
"2026-00-15",
|
|
576
|
-
"",
|
|
577
|
-
) { input ->
|
|
578
|
-
shouldThrow<DateParseException> {
|
|
579
|
-
parseDate(input)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
})
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### Test Lifecycle and Fixtures
|
|
587
|
-
|
|
588
|
-
#### BeforeTest / AfterTest
|
|
589
|
-
|
|
590
|
-
```kotlin
|
|
591
|
-
class DatabaseTest : FunSpec({
|
|
592
|
-
lateinit var db: Database
|
|
593
|
-
|
|
594
|
-
beforeSpec {
|
|
595
|
-
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
|
|
596
|
-
transaction(db) {
|
|
597
|
-
SchemaUtils.create(UsersTable)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
afterSpec {
|
|
602
|
-
transaction(db) {
|
|
603
|
-
SchemaUtils.drop(UsersTable)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
beforeTest {
|
|
608
|
-
transaction(db) {
|
|
609
|
-
UsersTable.deleteAll()
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
test("insert and retrieve user") {
|
|
614
|
-
transaction(db) {
|
|
615
|
-
UsersTable.insert {
|
|
616
|
-
it[name] = "Alice"
|
|
617
|
-
it[email] = "alice@example.com"
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
val users = transaction(db) {
|
|
622
|
-
UsersTable.selectAll().map { it[UsersTable.name] }
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
users shouldContain "Alice"
|
|
626
|
-
}
|
|
627
|
-
})
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
#### Kotest Extensions
|
|
631
|
-
|
|
632
|
-
```kotlin
|
|
633
|
-
// Reusable test extension
|
|
634
|
-
class DatabaseExtension : BeforeSpecListener, AfterSpecListener {
|
|
635
|
-
lateinit var db: Database
|
|
636
|
-
|
|
637
|
-
override suspend fun beforeSpec(spec: Spec) {
|
|
638
|
-
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
override suspend fun afterSpec(spec: Spec) {
|
|
642
|
-
// cleanup
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
class UserRepositoryTest : FunSpec({
|
|
647
|
-
val dbExt = DatabaseExtension()
|
|
648
|
-
register(dbExt)
|
|
649
|
-
|
|
650
|
-
test("save and find user") {
|
|
651
|
-
val repo = UserRepository(dbExt.db)
|
|
652
|
-
// ...
|
|
653
|
-
}
|
|
654
|
-
})
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### Kover Coverage
|
|
658
|
-
|
|
659
|
-
#### Gradle Configuration
|
|
660
|
-
|
|
661
|
-
```kotlin
|
|
662
|
-
// build.gradle.kts
|
|
663
|
-
plugins {
|
|
664
|
-
id("org.jetbrains.kotlinx.kover") version "0.9.7"
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
kover {
|
|
668
|
-
reports {
|
|
669
|
-
total {
|
|
670
|
-
html { onCheck = true }
|
|
671
|
-
xml { onCheck = true }
|
|
672
|
-
}
|
|
673
|
-
filters {
|
|
674
|
-
excludes {
|
|
675
|
-
classes("*.generated.*", "*.config.*")
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
verify {
|
|
679
|
-
rule {
|
|
680
|
-
minBound(80) // Fail build below 80% coverage
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
#### Coverage Commands
|
|
688
|
-
|
|
689
|
-
```bash
|
|
690
|
-
# Run tests with coverage
|
|
691
|
-
./gradlew koverHtmlReport
|
|
692
|
-
|
|
693
|
-
# Verify coverage thresholds
|
|
694
|
-
./gradlew koverVerify
|
|
695
|
-
|
|
696
|
-
# XML report for CI
|
|
697
|
-
./gradlew koverXmlReport
|
|
698
|
-
|
|
699
|
-
# View HTML report (use the command for your OS)
|
|
700
|
-
# macOS: open build/reports/kover/html/index.html
|
|
701
|
-
# Linux: xdg-open build/reports/kover/html/index.html
|
|
702
|
-
# Windows: start build/reports/kover/html/index.html
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
#### Coverage Targets
|
|
706
|
-
|
|
707
|
-
| Code Type | Target |
|
|
708
|
-
|-----------|--------|
|
|
709
|
-
| Critical business logic | 100% |
|
|
710
|
-
| Public APIs | 90%+ |
|
|
711
|
-
| General code | 80%+ |
|
|
712
|
-
| Generated / config code | Exclude |
|
|
713
|
-
|
|
714
|
-
### Ktor testApplication Testing
|
|
715
|
-
|
|
716
|
-
```kotlin
|
|
717
|
-
class ApiRoutesTest : FunSpec({
|
|
718
|
-
test("GET /users returns list") {
|
|
719
|
-
testApplication {
|
|
720
|
-
application {
|
|
721
|
-
configureRouting()
|
|
722
|
-
configureSerialization()
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
val response = client.get("/users")
|
|
726
|
-
|
|
727
|
-
response.status shouldBe HttpStatusCode.OK
|
|
728
|
-
val users = response.body<List<UserResponse>>()
|
|
729
|
-
users.shouldNotBeEmpty()
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
test("POST /users creates user") {
|
|
734
|
-
testApplication {
|
|
735
|
-
application {
|
|
736
|
-
configureRouting()
|
|
737
|
-
configureSerialization()
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
val response = client.post("/users") {
|
|
741
|
-
contentType(ContentType.Application.Json)
|
|
742
|
-
setBody(CreateUserRequest("Alice", "alice@example.com"))
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
response.status shouldBe HttpStatusCode.Created
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
})
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
### Testing Commands
|
|
752
|
-
|
|
753
|
-
```bash
|
|
754
|
-
# Run all tests
|
|
755
|
-
./gradlew test
|
|
756
|
-
|
|
757
|
-
# Run specific test class
|
|
758
|
-
./gradlew test --tests "com.example.UserServiceTest"
|
|
759
|
-
|
|
760
|
-
# Run specific test
|
|
761
|
-
./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
|
|
762
|
-
|
|
763
|
-
# Run with verbose output
|
|
764
|
-
./gradlew test --info
|
|
765
|
-
|
|
766
|
-
# Run with coverage
|
|
767
|
-
./gradlew koverHtmlReport
|
|
768
|
-
|
|
769
|
-
# Run detekt (static analysis)
|
|
770
|
-
./gradlew detekt
|
|
771
|
-
|
|
772
|
-
# Run ktlint (formatting check)
|
|
773
|
-
./gradlew ktlintCheck
|
|
774
|
-
|
|
775
|
-
# Continuous testing
|
|
776
|
-
./gradlew test --continuous
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
### Best Practices
|
|
780
|
-
|
|
781
|
-
**DO:**
|
|
782
|
-
- Write tests FIRST (TDD)
|
|
783
|
-
- Use Kotest's spec styles consistently across the project
|
|
784
|
-
- Use MockK's `coEvery`/`coVerify` for suspend functions
|
|
785
|
-
- Use `runTest` for coroutine testing
|
|
786
|
-
- Test behavior, not implementation
|
|
787
|
-
- Use property-based testing for pure functions
|
|
788
|
-
- Use `data class` test fixtures for clarity
|
|
789
|
-
|
|
790
|
-
**DON'T:**
|
|
791
|
-
- Mix testing frameworks (pick Kotest and stick with it)
|
|
792
|
-
- Mock data classes (use real instances)
|
|
793
|
-
- Use `Thread.sleep()` in coroutine tests (use `advanceTimeBy`)
|
|
794
|
-
- Skip the RED phase in TDD
|
|
795
|
-
- Test private functions directly
|
|
796
|
-
- Ignore flaky tests
|
|
797
|
-
|
|
798
|
-
### Integration with CI/CD
|
|
799
|
-
|
|
800
|
-
```yaml
|
|
801
|
-
# GitHub Actions example
|
|
802
|
-
test:
|
|
803
|
-
runs-on: ubuntu-latest
|
|
804
|
-
steps:
|
|
805
|
-
- uses: actions/checkout@v4
|
|
806
|
-
- uses: actions/setup-java@v4
|
|
807
|
-
with:
|
|
808
|
-
distribution: 'temurin'
|
|
809
|
-
java-version: '21'
|
|
810
|
-
|
|
811
|
-
- name: Run tests with coverage
|
|
812
|
-
run: ./gradlew test koverXmlReport
|
|
813
|
-
|
|
814
|
-
- name: Verify coverage
|
|
815
|
-
run: ./gradlew koverVerify
|
|
816
|
-
|
|
817
|
-
- name: Upload coverage
|
|
818
|
-
uses: codecov/codecov-action@v5
|
|
819
|
-
with:
|
|
820
|
-
files: build/reports/kover/report.xml
|
|
821
|
-
token: ${{ secrets.CODECOV_TOKEN }}
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
**Remember**: Tests are documentation. They show how your Kotlin code is meant to be used. Use Kotest's expressive matchers to make tests readable and MockK for clean mocking of dependencies.
|
|
1
|
+
---
|
|
2
|
+
name: kotlin-testing
|
|
3
|
+
description: Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Kotlin Testing Patterns
|
|
8
|
+
|
|
9
|
+
Comprehensive Kotlin testing patterns for writing reliable, maintainable tests following TDD methodology with Kotest and MockK.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- Writing new Kotlin functions or classes
|
|
14
|
+
- Adding test coverage to existing Kotlin code
|
|
15
|
+
- Implementing property-based tests
|
|
16
|
+
- Following TDD workflow in Kotlin projects
|
|
17
|
+
- Configuring Kover for code coverage
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
1. **Identify target code** — Find the function, class, or module to test
|
|
22
|
+
2. **Write a Kotest spec** — Choose a spec style (StringSpec, FunSpec, BehaviorSpec) matching the test scope
|
|
23
|
+
3. **Mock dependencies** — Use MockK to isolate the unit under test
|
|
24
|
+
4. **Run tests (RED)** — Verify the test fails with the expected error
|
|
25
|
+
5. **Implement code (GREEN)** — Write minimal code to pass the test
|
|
26
|
+
6. **Refactor** — Improve the implementation while keeping tests green
|
|
27
|
+
7. **Check coverage** — Run `./gradlew koverHtmlReport` and verify 80%+ coverage
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
The following sections contain detailed, runnable examples for each testing pattern:
|
|
32
|
+
|
|
33
|
+
### Quick Reference
|
|
34
|
+
|
|
35
|
+
- **Kotest specs** — StringSpec, FunSpec, BehaviorSpec, DescribeSpec examples in [Kotest Spec Styles](#kotest-spec-styles)
|
|
36
|
+
- **Mocking** — MockK setup, coroutine mocking, argument capture in [MockK](#mockk)
|
|
37
|
+
- **TDD walkthrough** — Full RED/GREEN/REFACTOR cycle with EmailValidator in [TDD Workflow for Kotlin](#tdd-workflow-for-kotlin)
|
|
38
|
+
- **Coverage** — Kover configuration and commands in [Kover Coverage](#kover-coverage)
|
|
39
|
+
- **Ktor testing** — testApplication setup in [Ktor testApplication Testing](#ktor-testapplication-testing)
|
|
40
|
+
|
|
41
|
+
### TDD Workflow for Kotlin
|
|
42
|
+
|
|
43
|
+
#### The RED-GREEN-REFACTOR Cycle
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
RED -> Write a failing test first
|
|
47
|
+
GREEN -> Write minimal code to pass the test
|
|
48
|
+
REFACTOR -> Improve code while keeping tests green
|
|
49
|
+
REPEAT -> Continue with next requirement
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Step-by-Step TDD in Kotlin
|
|
53
|
+
|
|
54
|
+
```kotlin
|
|
55
|
+
// Step 1: Define the interface/signature
|
|
56
|
+
// EmailValidator.kt
|
|
57
|
+
package com.example.validator
|
|
58
|
+
|
|
59
|
+
fun validateEmail(email: String): Result<String> {
|
|
60
|
+
TODO("not implemented")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Step 2: Write failing test (RED)
|
|
64
|
+
// EmailValidatorTest.kt
|
|
65
|
+
package com.example.validator
|
|
66
|
+
|
|
67
|
+
import io.kotest.core.spec.style.StringSpec
|
|
68
|
+
import io.kotest.matchers.result.shouldBeFailure
|
|
69
|
+
import io.kotest.matchers.result.shouldBeSuccess
|
|
70
|
+
|
|
71
|
+
class EmailValidatorTest : StringSpec({
|
|
72
|
+
"valid email returns success" {
|
|
73
|
+
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
"empty email returns failure" {
|
|
77
|
+
validateEmail("").shouldBeFailure()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
"email without @ returns failure" {
|
|
81
|
+
validateEmail("userexample.com").shouldBeFailure()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Step 3: Run tests - verify FAIL
|
|
86
|
+
// $ ./gradlew test
|
|
87
|
+
// EmailValidatorTest > valid email returns success FAILED
|
|
88
|
+
// kotlin.NotImplementedError: An operation is not implemented
|
|
89
|
+
|
|
90
|
+
// Step 4: Implement minimal code (GREEN)
|
|
91
|
+
fun validateEmail(email: String): Result<String> {
|
|
92
|
+
if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
|
|
93
|
+
if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
|
|
94
|
+
val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
|
95
|
+
if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
|
|
96
|
+
return Result.success(email)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Step 5: Run tests - verify PASS
|
|
100
|
+
// $ ./gradlew test
|
|
101
|
+
// EmailValidatorTest > valid email returns success PASSED
|
|
102
|
+
// EmailValidatorTest > empty email returns failure PASSED
|
|
103
|
+
// EmailValidatorTest > email without @ returns failure PASSED
|
|
104
|
+
|
|
105
|
+
// Step 6: Refactor if needed, verify tests still pass
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Kotest Spec Styles
|
|
109
|
+
|
|
110
|
+
#### StringSpec (Simplest)
|
|
111
|
+
|
|
112
|
+
```kotlin
|
|
113
|
+
class CalculatorTest : StringSpec({
|
|
114
|
+
"add two positive numbers" {
|
|
115
|
+
Calculator.add(2, 3) shouldBe 5
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
"add negative numbers" {
|
|
119
|
+
Calculator.add(-1, -2) shouldBe -3
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
"add zero" {
|
|
123
|
+
Calculator.add(0, 5) shouldBe 5
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### FunSpec (JUnit-like)
|
|
129
|
+
|
|
130
|
+
```kotlin
|
|
131
|
+
class UserServiceTest : FunSpec({
|
|
132
|
+
val repository = mockk<UserRepository>()
|
|
133
|
+
val service = UserService(repository)
|
|
134
|
+
|
|
135
|
+
test("getUser returns user when found") {
|
|
136
|
+
val expected = User(id = "1", name = "Alice")
|
|
137
|
+
coEvery { repository.findById("1") } returns expected
|
|
138
|
+
|
|
139
|
+
val result = service.getUser("1")
|
|
140
|
+
|
|
141
|
+
result shouldBe expected
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test("getUser throws when not found") {
|
|
145
|
+
coEvery { repository.findById("999") } returns null
|
|
146
|
+
|
|
147
|
+
shouldThrow<UserNotFoundException> {
|
|
148
|
+
service.getUser("999")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### BehaviorSpec (BDD Style)
|
|
155
|
+
|
|
156
|
+
```kotlin
|
|
157
|
+
class OrderServiceTest : BehaviorSpec({
|
|
158
|
+
val repository = mockk<OrderRepository>()
|
|
159
|
+
val paymentService = mockk<PaymentService>()
|
|
160
|
+
val service = OrderService(repository, paymentService)
|
|
161
|
+
|
|
162
|
+
Given("a valid order request") {
|
|
163
|
+
val request = CreateOrderRequest(
|
|
164
|
+
userId = "user-1",
|
|
165
|
+
items = listOf(OrderItem("product-1", quantity = 2)),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
When("the order is placed") {
|
|
169
|
+
coEvery { paymentService.charge(any()) } returns PaymentResult.Success
|
|
170
|
+
coEvery { repository.save(any()) } answers { firstArg() }
|
|
171
|
+
|
|
172
|
+
val result = service.placeOrder(request)
|
|
173
|
+
|
|
174
|
+
Then("it should return a confirmed order") {
|
|
175
|
+
result.status shouldBe OrderStatus.CONFIRMED
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Then("it should charge payment") {
|
|
179
|
+
coVerify(exactly = 1) { paymentService.charge(any()) }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
When("payment fails") {
|
|
184
|
+
coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
|
|
185
|
+
|
|
186
|
+
Then("it should throw PaymentException") {
|
|
187
|
+
shouldThrow<PaymentException> {
|
|
188
|
+
service.placeOrder(request)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### DescribeSpec (RSpec Style)
|
|
197
|
+
|
|
198
|
+
```kotlin
|
|
199
|
+
class UserValidatorTest : DescribeSpec({
|
|
200
|
+
describe("validateUser") {
|
|
201
|
+
val validator = UserValidator()
|
|
202
|
+
|
|
203
|
+
context("with valid input") {
|
|
204
|
+
it("accepts a normal user") {
|
|
205
|
+
val user = CreateUserRequest("Alice", "alice@example.com")
|
|
206
|
+
validator.validate(user).shouldBeValid()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
context("with invalid name") {
|
|
211
|
+
it("rejects blank name") {
|
|
212
|
+
val user = CreateUserRequest("", "alice@example.com")
|
|
213
|
+
validator.validate(user).shouldBeInvalid()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
it("rejects name exceeding max length") {
|
|
217
|
+
val user = CreateUserRequest("A".repeat(256), "alice@example.com")
|
|
218
|
+
validator.validate(user).shouldBeInvalid()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Kotest Matchers
|
|
226
|
+
|
|
227
|
+
#### Core Matchers
|
|
228
|
+
|
|
229
|
+
```kotlin
|
|
230
|
+
import io.kotest.matchers.shouldBe
|
|
231
|
+
import io.kotest.matchers.shouldNotBe
|
|
232
|
+
import io.kotest.matchers.string.*
|
|
233
|
+
import io.kotest.matchers.collections.*
|
|
234
|
+
import io.kotest.matchers.nulls.*
|
|
235
|
+
|
|
236
|
+
// Equality
|
|
237
|
+
result shouldBe expected
|
|
238
|
+
result shouldNotBe unexpected
|
|
239
|
+
|
|
240
|
+
// Strings
|
|
241
|
+
name shouldStartWith "Al"
|
|
242
|
+
name shouldEndWith "ice"
|
|
243
|
+
name shouldContain "lic"
|
|
244
|
+
name shouldMatch Regex("[A-Z][a-z]+")
|
|
245
|
+
name.shouldBeBlank()
|
|
246
|
+
|
|
247
|
+
// Collections
|
|
248
|
+
list shouldContain "item"
|
|
249
|
+
list shouldHaveSize 3
|
|
250
|
+
list.shouldBeSorted()
|
|
251
|
+
list.shouldContainAll("a", "b", "c")
|
|
252
|
+
list.shouldBeEmpty()
|
|
253
|
+
|
|
254
|
+
// Nulls
|
|
255
|
+
result.shouldNotBeNull()
|
|
256
|
+
result.shouldBeNull()
|
|
257
|
+
|
|
258
|
+
// Types
|
|
259
|
+
result.shouldBeInstanceOf<User>()
|
|
260
|
+
|
|
261
|
+
// Numbers
|
|
262
|
+
count shouldBeGreaterThan 0
|
|
263
|
+
price shouldBeInRange 1.0..100.0
|
|
264
|
+
|
|
265
|
+
// Exceptions
|
|
266
|
+
shouldThrow<IllegalArgumentException> {
|
|
267
|
+
validateAge(-1)
|
|
268
|
+
}.message shouldBe "Age must be positive"
|
|
269
|
+
|
|
270
|
+
shouldNotThrow<Exception> {
|
|
271
|
+
validateAge(25)
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Custom Matchers
|
|
276
|
+
|
|
277
|
+
```kotlin
|
|
278
|
+
fun beActiveUser() = object : Matcher<User> {
|
|
279
|
+
override fun test(value: User) = MatcherResult(
|
|
280
|
+
value.isActive && value.lastLogin != null,
|
|
281
|
+
{ "User ${value.id} should be active with a last login" },
|
|
282
|
+
{ "User ${value.id} should not be active" },
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Usage
|
|
287
|
+
user should beActiveUser()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### MockK
|
|
291
|
+
|
|
292
|
+
#### Basic Mocking
|
|
293
|
+
|
|
294
|
+
```kotlin
|
|
295
|
+
class UserServiceTest : FunSpec({
|
|
296
|
+
val repository = mockk<UserRepository>()
|
|
297
|
+
val logger = mockk<Logger>(relaxed = true) // Relaxed: returns defaults
|
|
298
|
+
val service = UserService(repository, logger)
|
|
299
|
+
|
|
300
|
+
beforeTest {
|
|
301
|
+
clearMocks(repository, logger)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
test("findUser delegates to repository") {
|
|
305
|
+
val expected = User(id = "1", name = "Alice")
|
|
306
|
+
every { repository.findById("1") } returns expected
|
|
307
|
+
|
|
308
|
+
val result = service.findUser("1")
|
|
309
|
+
|
|
310
|
+
result shouldBe expected
|
|
311
|
+
verify(exactly = 1) { repository.findById("1") }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
test("findUser returns null for unknown id") {
|
|
315
|
+
every { repository.findById(any()) } returns null
|
|
316
|
+
|
|
317
|
+
val result = service.findUser("unknown")
|
|
318
|
+
|
|
319
|
+
result.shouldBeNull()
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
#### Coroutine Mocking
|
|
325
|
+
|
|
326
|
+
```kotlin
|
|
327
|
+
class AsyncUserServiceTest : FunSpec({
|
|
328
|
+
val repository = mockk<UserRepository>()
|
|
329
|
+
val service = UserService(repository)
|
|
330
|
+
|
|
331
|
+
test("getUser suspending function") {
|
|
332
|
+
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
|
|
333
|
+
|
|
334
|
+
val result = service.getUser("1")
|
|
335
|
+
|
|
336
|
+
result.name shouldBe "Alice"
|
|
337
|
+
coVerify { repository.findById("1") }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
test("getUser with delay") {
|
|
341
|
+
coEvery { repository.findById("1") } coAnswers {
|
|
342
|
+
delay(100) // Simulate async work
|
|
343
|
+
User(id = "1", name = "Alice")
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
val result = service.getUser("1")
|
|
347
|
+
result.name shouldBe "Alice"
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Argument Capture
|
|
353
|
+
|
|
354
|
+
```kotlin
|
|
355
|
+
test("save captures the user argument") {
|
|
356
|
+
val slot = slot<User>()
|
|
357
|
+
coEvery { repository.save(capture(slot)) } returns Unit
|
|
358
|
+
|
|
359
|
+
service.createUser(CreateUserRequest("Alice", "alice@example.com"))
|
|
360
|
+
|
|
361
|
+
slot.captured.name shouldBe "Alice"
|
|
362
|
+
slot.captured.email shouldBe "alice@example.com"
|
|
363
|
+
slot.captured.id.shouldNotBeNull()
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Spy and Partial Mocking
|
|
368
|
+
|
|
369
|
+
```kotlin
|
|
370
|
+
test("spy on real object") {
|
|
371
|
+
val realService = UserService(repository)
|
|
372
|
+
val spy = spyk(realService)
|
|
373
|
+
|
|
374
|
+
every { spy.generateId() } returns "fixed-id"
|
|
375
|
+
|
|
376
|
+
spy.createUser(request)
|
|
377
|
+
|
|
378
|
+
verify { spy.generateId() } // Overridden
|
|
379
|
+
// Other methods use real implementation
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Coroutine Testing
|
|
384
|
+
|
|
385
|
+
#### runTest for Suspend Functions
|
|
386
|
+
|
|
387
|
+
```kotlin
|
|
388
|
+
import kotlinx.coroutines.test.runTest
|
|
389
|
+
|
|
390
|
+
class CoroutineServiceTest : FunSpec({
|
|
391
|
+
test("concurrent fetches complete together") {
|
|
392
|
+
runTest {
|
|
393
|
+
val service = DataService(testScope = this)
|
|
394
|
+
|
|
395
|
+
val result = service.fetchAllData()
|
|
396
|
+
|
|
397
|
+
result.users.shouldNotBeEmpty()
|
|
398
|
+
result.products.shouldNotBeEmpty()
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
test("timeout after delay") {
|
|
403
|
+
runTest {
|
|
404
|
+
val service = SlowService()
|
|
405
|
+
|
|
406
|
+
shouldThrow<TimeoutCancellationException> {
|
|
407
|
+
withTimeout(100) {
|
|
408
|
+
service.slowOperation() // Takes > 100ms
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### Testing Flows
|
|
417
|
+
|
|
418
|
+
```kotlin
|
|
419
|
+
import io.kotest.matchers.collections.shouldContainInOrder
|
|
420
|
+
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
421
|
+
import kotlinx.coroutines.flow.toList
|
|
422
|
+
import kotlinx.coroutines.launch
|
|
423
|
+
import kotlinx.coroutines.test.advanceTimeBy
|
|
424
|
+
import kotlinx.coroutines.test.runTest
|
|
425
|
+
|
|
426
|
+
class FlowServiceTest : FunSpec({
|
|
427
|
+
test("observeUsers emits updates") {
|
|
428
|
+
runTest {
|
|
429
|
+
val service = UserFlowService()
|
|
430
|
+
|
|
431
|
+
val emissions = service.observeUsers()
|
|
432
|
+
.take(3)
|
|
433
|
+
.toList()
|
|
434
|
+
|
|
435
|
+
emissions shouldHaveSize 3
|
|
436
|
+
emissions.last().shouldNotBeEmpty()
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
test("searchUsers debounces input") {
|
|
441
|
+
runTest {
|
|
442
|
+
val service = SearchService()
|
|
443
|
+
val queries = MutableSharedFlow<String>()
|
|
444
|
+
|
|
445
|
+
val results = mutableListOf<List<User>>()
|
|
446
|
+
val job = launch {
|
|
447
|
+
service.searchUsers(queries).collect { results.add(it) }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
queries.emit("a")
|
|
451
|
+
queries.emit("ab")
|
|
452
|
+
queries.emit("abc") // Only this should trigger search
|
|
453
|
+
advanceTimeBy(500)
|
|
454
|
+
|
|
455
|
+
results shouldHaveSize 1
|
|
456
|
+
job.cancel()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
#### TestDispatcher
|
|
463
|
+
|
|
464
|
+
```kotlin
|
|
465
|
+
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
466
|
+
import kotlinx.coroutines.test.advanceUntilIdle
|
|
467
|
+
|
|
468
|
+
class DispatcherTest : FunSpec({
|
|
469
|
+
test("uses test dispatcher for controlled execution") {
|
|
470
|
+
val dispatcher = StandardTestDispatcher()
|
|
471
|
+
|
|
472
|
+
runTest(dispatcher) {
|
|
473
|
+
var completed = false
|
|
474
|
+
|
|
475
|
+
launch {
|
|
476
|
+
delay(1000)
|
|
477
|
+
completed = true
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
completed shouldBe false
|
|
481
|
+
advanceTimeBy(1000)
|
|
482
|
+
completed shouldBe true
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Property-Based Testing
|
|
489
|
+
|
|
490
|
+
#### Kotest Property Testing
|
|
491
|
+
|
|
492
|
+
```kotlin
|
|
493
|
+
import io.kotest.core.spec.style.FunSpec
|
|
494
|
+
import io.kotest.property.Arb
|
|
495
|
+
import io.kotest.property.arbitrary.*
|
|
496
|
+
import io.kotest.property.forAll
|
|
497
|
+
import io.kotest.property.checkAll
|
|
498
|
+
import kotlinx.serialization.json.Json
|
|
499
|
+
import kotlinx.serialization.encodeToString
|
|
500
|
+
import kotlinx.serialization.decodeFromString
|
|
501
|
+
|
|
502
|
+
// Note: The serialization roundtrip test below requires the User data class
|
|
503
|
+
// to be annotated with @Serializable (from kotlinx.serialization).
|
|
504
|
+
|
|
505
|
+
class PropertyTest : FunSpec({
|
|
506
|
+
test("string reverse is involutory") {
|
|
507
|
+
forAll<String> { s ->
|
|
508
|
+
s.reversed().reversed() == s
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
test("list sort is idempotent") {
|
|
513
|
+
forAll(Arb.list(Arb.int())) { list ->
|
|
514
|
+
list.sorted() == list.sorted().sorted()
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
test("serialization roundtrip preserves data") {
|
|
519
|
+
checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
|
|
520
|
+
User(name = name, email = "$email@test.com")
|
|
521
|
+
}) { user ->
|
|
522
|
+
val json = Json.encodeToString(user)
|
|
523
|
+
val decoded = Json.decodeFromString<User>(json)
|
|
524
|
+
decoded shouldBe user
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### Custom Generators
|
|
531
|
+
|
|
532
|
+
```kotlin
|
|
533
|
+
val userArb: Arb<User> = Arb.bind(
|
|
534
|
+
Arb.string(minSize = 1, maxSize = 50),
|
|
535
|
+
Arb.email(),
|
|
536
|
+
Arb.enum<Role>(),
|
|
537
|
+
) { name, email, role ->
|
|
538
|
+
User(
|
|
539
|
+
id = UserId(UUID.randomUUID().toString()),
|
|
540
|
+
name = name,
|
|
541
|
+
email = Email(email),
|
|
542
|
+
role = role,
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
val moneyArb: Arb<Money> = Arb.bind(
|
|
547
|
+
Arb.long(1L..1_000_000L),
|
|
548
|
+
Arb.enum<Currency>(),
|
|
549
|
+
) { amount, currency ->
|
|
550
|
+
Money(amount, currency)
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Data-Driven Testing
|
|
555
|
+
|
|
556
|
+
#### withData in Kotest
|
|
557
|
+
|
|
558
|
+
```kotlin
|
|
559
|
+
class ParserTest : FunSpec({
|
|
560
|
+
context("parsing valid dates") {
|
|
561
|
+
withData(
|
|
562
|
+
"2026-01-15" to LocalDate(2026, 1, 15),
|
|
563
|
+
"2026-12-31" to LocalDate(2026, 12, 31),
|
|
564
|
+
"2000-01-01" to LocalDate(2000, 1, 1),
|
|
565
|
+
) { (input, expected) ->
|
|
566
|
+
parseDate(input) shouldBe expected
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
context("rejecting invalid dates") {
|
|
571
|
+
withData(
|
|
572
|
+
nameFn = { "rejects '$it'" },
|
|
573
|
+
"not-a-date",
|
|
574
|
+
"2026-13-01",
|
|
575
|
+
"2026-00-15",
|
|
576
|
+
"",
|
|
577
|
+
) { input ->
|
|
578
|
+
shouldThrow<DateParseException> {
|
|
579
|
+
parseDate(input)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Test Lifecycle and Fixtures
|
|
587
|
+
|
|
588
|
+
#### BeforeTest / AfterTest
|
|
589
|
+
|
|
590
|
+
```kotlin
|
|
591
|
+
class DatabaseTest : FunSpec({
|
|
592
|
+
lateinit var db: Database
|
|
593
|
+
|
|
594
|
+
beforeSpec {
|
|
595
|
+
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
|
|
596
|
+
transaction(db) {
|
|
597
|
+
SchemaUtils.create(UsersTable)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
afterSpec {
|
|
602
|
+
transaction(db) {
|
|
603
|
+
SchemaUtils.drop(UsersTable)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
beforeTest {
|
|
608
|
+
transaction(db) {
|
|
609
|
+
UsersTable.deleteAll()
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
test("insert and retrieve user") {
|
|
614
|
+
transaction(db) {
|
|
615
|
+
UsersTable.insert {
|
|
616
|
+
it[name] = "Alice"
|
|
617
|
+
it[email] = "alice@example.com"
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
val users = transaction(db) {
|
|
622
|
+
UsersTable.selectAll().map { it[UsersTable.name] }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
users shouldContain "Alice"
|
|
626
|
+
}
|
|
627
|
+
})
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
#### Kotest Extensions
|
|
631
|
+
|
|
632
|
+
```kotlin
|
|
633
|
+
// Reusable test extension
|
|
634
|
+
class DatabaseExtension : BeforeSpecListener, AfterSpecListener {
|
|
635
|
+
lateinit var db: Database
|
|
636
|
+
|
|
637
|
+
override suspend fun beforeSpec(spec: Spec) {
|
|
638
|
+
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
override suspend fun afterSpec(spec: Spec) {
|
|
642
|
+
// cleanup
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
class UserRepositoryTest : FunSpec({
|
|
647
|
+
val dbExt = DatabaseExtension()
|
|
648
|
+
register(dbExt)
|
|
649
|
+
|
|
650
|
+
test("save and find user") {
|
|
651
|
+
val repo = UserRepository(dbExt.db)
|
|
652
|
+
// ...
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Kover Coverage
|
|
658
|
+
|
|
659
|
+
#### Gradle Configuration
|
|
660
|
+
|
|
661
|
+
```kotlin
|
|
662
|
+
// build.gradle.kts
|
|
663
|
+
plugins {
|
|
664
|
+
id("org.jetbrains.kotlinx.kover") version "0.9.7"
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
kover {
|
|
668
|
+
reports {
|
|
669
|
+
total {
|
|
670
|
+
html { onCheck = true }
|
|
671
|
+
xml { onCheck = true }
|
|
672
|
+
}
|
|
673
|
+
filters {
|
|
674
|
+
excludes {
|
|
675
|
+
classes("*.generated.*", "*.config.*")
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
verify {
|
|
679
|
+
rule {
|
|
680
|
+
minBound(80) // Fail build below 80% coverage
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
#### Coverage Commands
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
# Run tests with coverage
|
|
691
|
+
./gradlew koverHtmlReport
|
|
692
|
+
|
|
693
|
+
# Verify coverage thresholds
|
|
694
|
+
./gradlew koverVerify
|
|
695
|
+
|
|
696
|
+
# XML report for CI
|
|
697
|
+
./gradlew koverXmlReport
|
|
698
|
+
|
|
699
|
+
# View HTML report (use the command for your OS)
|
|
700
|
+
# macOS: open build/reports/kover/html/index.html
|
|
701
|
+
# Linux: xdg-open build/reports/kover/html/index.html
|
|
702
|
+
# Windows: start build/reports/kover/html/index.html
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
#### Coverage Targets
|
|
706
|
+
|
|
707
|
+
| Code Type | Target |
|
|
708
|
+
|-----------|--------|
|
|
709
|
+
| Critical business logic | 100% |
|
|
710
|
+
| Public APIs | 90%+ |
|
|
711
|
+
| General code | 80%+ |
|
|
712
|
+
| Generated / config code | Exclude |
|
|
713
|
+
|
|
714
|
+
### Ktor testApplication Testing
|
|
715
|
+
|
|
716
|
+
```kotlin
|
|
717
|
+
class ApiRoutesTest : FunSpec({
|
|
718
|
+
test("GET /users returns list") {
|
|
719
|
+
testApplication {
|
|
720
|
+
application {
|
|
721
|
+
configureRouting()
|
|
722
|
+
configureSerialization()
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
val response = client.get("/users")
|
|
726
|
+
|
|
727
|
+
response.status shouldBe HttpStatusCode.OK
|
|
728
|
+
val users = response.body<List<UserResponse>>()
|
|
729
|
+
users.shouldNotBeEmpty()
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
test("POST /users creates user") {
|
|
734
|
+
testApplication {
|
|
735
|
+
application {
|
|
736
|
+
configureRouting()
|
|
737
|
+
configureSerialization()
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
val response = client.post("/users") {
|
|
741
|
+
contentType(ContentType.Application.Json)
|
|
742
|
+
setBody(CreateUserRequest("Alice", "alice@example.com"))
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
response.status shouldBe HttpStatusCode.Created
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
})
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Testing Commands
|
|
752
|
+
|
|
753
|
+
```bash
|
|
754
|
+
# Run all tests
|
|
755
|
+
./gradlew test
|
|
756
|
+
|
|
757
|
+
# Run specific test class
|
|
758
|
+
./gradlew test --tests "com.example.UserServiceTest"
|
|
759
|
+
|
|
760
|
+
# Run specific test
|
|
761
|
+
./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
|
|
762
|
+
|
|
763
|
+
# Run with verbose output
|
|
764
|
+
./gradlew test --info
|
|
765
|
+
|
|
766
|
+
# Run with coverage
|
|
767
|
+
./gradlew koverHtmlReport
|
|
768
|
+
|
|
769
|
+
# Run detekt (static analysis)
|
|
770
|
+
./gradlew detekt
|
|
771
|
+
|
|
772
|
+
# Run ktlint (formatting check)
|
|
773
|
+
./gradlew ktlintCheck
|
|
774
|
+
|
|
775
|
+
# Continuous testing
|
|
776
|
+
./gradlew test --continuous
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Best Practices
|
|
780
|
+
|
|
781
|
+
**DO:**
|
|
782
|
+
- Write tests FIRST (TDD)
|
|
783
|
+
- Use Kotest's spec styles consistently across the project
|
|
784
|
+
- Use MockK's `coEvery`/`coVerify` for suspend functions
|
|
785
|
+
- Use `runTest` for coroutine testing
|
|
786
|
+
- Test behavior, not implementation
|
|
787
|
+
- Use property-based testing for pure functions
|
|
788
|
+
- Use `data class` test fixtures for clarity
|
|
789
|
+
|
|
790
|
+
**DON'T:**
|
|
791
|
+
- Mix testing frameworks (pick Kotest and stick with it)
|
|
792
|
+
- Mock data classes (use real instances)
|
|
793
|
+
- Use `Thread.sleep()` in coroutine tests (use `advanceTimeBy`)
|
|
794
|
+
- Skip the RED phase in TDD
|
|
795
|
+
- Test private functions directly
|
|
796
|
+
- Ignore flaky tests
|
|
797
|
+
|
|
798
|
+
### Integration with CI/CD
|
|
799
|
+
|
|
800
|
+
```yaml
|
|
801
|
+
# GitHub Actions example
|
|
802
|
+
test:
|
|
803
|
+
runs-on: ubuntu-latest
|
|
804
|
+
steps:
|
|
805
|
+
- uses: actions/checkout@v4
|
|
806
|
+
- uses: actions/setup-java@v4
|
|
807
|
+
with:
|
|
808
|
+
distribution: 'temurin'
|
|
809
|
+
java-version: '21'
|
|
810
|
+
|
|
811
|
+
- name: Run tests with coverage
|
|
812
|
+
run: ./gradlew test koverXmlReport
|
|
813
|
+
|
|
814
|
+
- name: Verify coverage
|
|
815
|
+
run: ./gradlew koverVerify
|
|
816
|
+
|
|
817
|
+
- name: Upload coverage
|
|
818
|
+
uses: codecov/codecov-action@v5
|
|
819
|
+
with:
|
|
820
|
+
files: build/reports/kover/report.xml
|
|
821
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Remember**: Tests are documentation. They show how your Kotlin code is meant to be used. Use Kotest's expressive matchers to make tests readable and MockK for clean mocking of dependencies.
|