kotlin-testing
Kotest・MockK・コルーチンテスト・プロパティベーステスト・Kover カバレッジを活用した Kotlin のテストパターンに対応するスキル。イディオマティックな Kotlin の実装を通じて TDD メソドロジーに従い、品質の高いテストコードの作成をサポートします。
description の原文を見る
Kotest, MockK, coroutine testi, property-based testing ve Kover coverage ile Kotlin test kalıpları. İdiomatic Kotlin uygulamalarıyla TDD metodolojisini takip eder.
SKILL.md 本文
Kotlin テストパターン
Kotest と MockK を使用して TDD 方法論に従い、信頼性が高く保守可能なテストを作成するための包括的な Kotlin テストパターン。
いつ使用するか
- 新しい Kotlin 関数またはクラスを作成する時
- 既存の Kotlin コードにテストカバレッジを追加する時
- プロパティベースのテストを実装する時
- Kotlin プロジェクトで TDD ワークフローに従う時
- コードカバレッジのために Kover を設定する時
仕組み
- 対象コードを特定する — テストする関数、クラス、またはモジュールを見つける
- Kotest spec を作成する — テストスコープに適した spec スタイルを選択 (StringSpec、FunSpec、BehaviorSpec)
- 依存関係をモック化する — テスト対象の単体を分離するために MockK を使用
- テストを実行する (RED) — テストが期待されたエラーで失敗することを確認
- コードを実装する (GREEN) — テストをパスするための最小限のコードを作成
- リファクタリング — テストを緑色に保ちながら実装を改善
- カバレッジを確認する —
./gradlew koverHtmlReportを実行して 80% 以上のカバレッジを確認
Kotlin の TDD ワークフロー
RED-GREEN-REFACTOR サイクル
RED -> まず失敗するテストを作成
GREEN -> テストをパスするための最小限のコードを作成
REFACTOR -> テストを緑色に保ちながらコードを改善
REPEAT -> 次の要件で続行
Kotlin でのステップバイステップ TDD
// ステップ 1: インターフェース/シグネチャを定義
// EmailValidator.kt
package com.example.validator
fun validateEmail(email: String): Result<String> {
TODO("not implemented")
}
// ステップ 2: 失敗するテストを作成 (RED)
// EmailValidatorTest.kt
package com.example.validator
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.result.shouldBeFailure
import io.kotest.matchers.result.shouldBeSuccess
class EmailValidatorTest : StringSpec({
"valid email returns success" {
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
}
"empty email returns failure" {
validateEmail("").shouldBeFailure()
}
"email without @ returns failure" {
validateEmail("userexample.com").shouldBeFailure()
}
})
// ステップ 3: テストを実行 - FAIL を確認
// $ ./gradlew test
// EmailValidatorTest > valid email returns success FAILED
// kotlin.NotImplementedError: An operation is not implemented
// ステップ 4: 最小限のコードを実装 (GREEN)
fun validateEmail(email: String): Result<String> {
if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
return Result.success(email)
}
// ステップ 5: テストを実行 - PASS を確認
// $ ./gradlew test
// EmailValidatorTest > valid email returns success PASSED
// EmailValidatorTest > empty email returns failure PASSED
// EmailValidatorTest > email without @ returns failure PASSED
// ステップ 6: 必要に応じてリファクタリング、テストがまだパスすることを確認
Kotest Spec スタイル
StringSpec (最もシンプル)
class CalculatorTest : StringSpec({
"add two positive numbers" {
Calculator.add(2, 3) shouldBe 5
}
"add negative numbers" {
Calculator.add(-1, -2) shouldBe -3
}
"add zero" {
Calculator.add(0, 5) shouldBe 5
}
})
FunSpec (JUnit のような)
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
test("getUser returns user when found") {
val expected = User(id = "1", name = "Alice")
coEvery { repository.findById("1") } returns expected
val result = service.getUser("1")
result shouldBe expected
}
test("getUser throws when not found") {
coEvery { repository.findById("999") } returns null
shouldThrow<UserNotFoundException> {
service.getUser("999")
}
}
})
BehaviorSpec (BDD スタイル)
class OrderServiceTest : BehaviorSpec({
val repository = mockk<OrderRepository>()
val paymentService = mockk<PaymentService>()
val service = OrderService(repository, paymentService)
Given("a valid order request") {
val request = CreateOrderRequest(
userId = "user-1",
items = listOf(OrderItem("product-1", quantity = 2)),
)
When("the order is placed") {
coEvery { paymentService.charge(any()) } returns PaymentResult.Success
coEvery { repository.save(any()) } answers { firstArg() }
val result = service.placeOrder(request)
Then("it should return a confirmed order") {
result.status shouldBe OrderStatus.CONFIRMED
}
Then("it should charge payment") {
coVerify(exactly = 1) { paymentService.charge(any()) }
}
}
When("payment fails") {
coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
Then("it should throw PaymentException") {
shouldThrow<PaymentException> {
service.placeOrder(request)
}
}
}
}
})
Kotest マッチャー
基本的なマッチャー
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.*
import io.kotest.matchers.collections.*
import io.kotest.matchers.nulls.*
// 等値性
result shouldBe expected
result shouldNotBe unexpected
// 文字列
name shouldStartWith "Al"
name shouldEndWith "ice"
name shouldContain "lic"
name shouldMatch Regex("[A-Z][a-z]+")
name.shouldBeBlank()
// コレクション
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
list.shouldContainAll("a", "b", "c")
list.shouldBeEmpty()
// Null
result.shouldNotBeNull()
result.shouldBeNull()
// 型
result.shouldBeInstanceOf<User>()
// 数値
count shouldBeGreaterThan 0
price shouldBeInRange 1.0..100.0
// 例外
shouldThrow<IllegalArgumentException> {
validateAge(-1)
}.message shouldBe "Age must be positive"
shouldNotThrow<Exception> {
validateAge(25)
}
MockK
基本的なモッキング
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val logger = mockk<Logger>(relaxed = true) // Relaxed: デフォルト値を返す
val service = UserService(repository, logger)
beforeTest {
clearMocks(repository, logger)
}
test("findUser delegates to repository") {
val expected = User(id = "1", name = "Alice")
every { repository.findById("1") } returns expected
val result = service.findUser("1")
result shouldBe expected
verify(exactly = 1) { repository.findById("1") }
}
test("findUser returns null for unknown id") {
every { repository.findById(any()) } returns null
val result = service.findUser("unknown")
result.shouldBeNull()
}
})
コルーチンのモッキング
class AsyncUserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
test("getUser suspending function") {
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
val result = service.getUser("1")
result.name shouldBe "Alice"
coVerify { repository.findById("1") }
}
test("getUser with delay") {
coEvery { repository.findById("1") } coAnswers {
delay(100) // 非同期処理をシミュレート
User(id = "1", name = "Alice")
}
val result = service.getUser("1")
result.name shouldBe "Alice"
}
})
コルーチンテスト
サスペンド関数用の runTest
import kotlinx.coroutines.test.runTest
class CoroutineServiceTest : FunSpec({
test("concurrent fetches complete together") {
runTest {
val service = DataService(testScope = this)
val result = service.fetchAllData()
result.users.shouldNotBeEmpty()
result.products.shouldNotBeEmpty()
}
}
test("timeout after delay") {
runTest {
val service = SlowService()
shouldThrow<TimeoutCancellationException> {
withTimeout(100) {
service.slowOperation() // > 100ms かかる
}
}
}
}
})
Flow テスト
import io.kotest.matchers.collections.shouldContainInOrder
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
class FlowServiceTest : FunSpec({
test("observeUsers emits updates") {
runTest {
val service = UserFlowService()
val emissions = service.observeUsers()
.take(3)
.toList()
emissions shouldHaveSize 3
emissions.last().shouldNotBeEmpty()
}
}
test("searchUsers debounces input") {
runTest {
val service = SearchService()
val queries = MutableSharedFlow<String>()
val results = mutableListOf<List<User>>()
val job = launch {
service.searchUsers(queries).collect { results.add(it) }
}
queries.emit("a")
queries.emit("ab")
queries.emit("abc") // このクエリのみをトリガーする
advanceTimeBy(500)
results shouldHaveSize 1
job.cancel()
}
}
})
プロパティベースのテスト
Kotest プロパティテスト
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.forAll
import io.kotest.property.checkAll
class PropertyTest : FunSpec({
test("string reverse is involutory") {
forAll<String> { s ->
s.reversed().reversed() == s
}
}
test("list sort is idempotent") {
forAll(Arb.list(Arb.int())) { list ->
list.sorted() == list.sorted().sorted()
}
}
test("serialization roundtrip preserves data") {
checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
User(name = name, email = "$email@test.com")
}) { user ->
val json = Json.encodeToString(user)
val decoded = Json.decodeFromString<User>(json)
decoded shouldBe user
}
}
})
Kover カバレッジ
Gradle 設定
// build.gradle.kts
plugins {
id("org.jetbrains.kotlinx.kover") version "0.9.7"
}
kover {
reports {
total {
html { onCheck = true }
xml { onCheck = true }
}
filters {
excludes {
classes("*.generated.*", "*.config.*")
}
}
verify {
rule {
minBound(80) // 80% のカバレッジ未満でビルド失敗
}
}
}
}
カバレッジコマンド
# カバレッジ付きでテストを実行
./gradlew koverHtmlReport
# カバレッジしきい値を検証
./gradlew koverVerify
# CI 用の XML レポート
./gradlew koverXmlReport
# HTML レポートを表示 (OS に応じてコマンドを使用)
# macOS: open build/reports/kover/html/index.html
# Linux: xdg-open build/reports/kover/html/index.html
# Windows: start build/reports/kover/html/index.html
カバレッジ目標
| コードタイプ | 目標 |
|---|---|
| クリティカルなビジネスロジック | %100 |
| パブリック API | %90+ |
| 一般的なコード | %80+ |
| 生成コード / 設定コード | 除外 |
Ktor testApplication テスト
class ApiRoutesTest : FunSpec({
test("GET /users returns list") {
testApplication {
application {
configureRouting()
configureSerialization()
}
val response = client.get("/users")
response.status shouldBe HttpStatusCode.OK
val users = response.body<List<UserResponse>>()
users.shouldNotBeEmpty()
}
}
test("POST /users creates user") {
testApplication {
application {
configureRouting()
configureSerialization()
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
setBody(CreateUserRequest("Alice", "alice@example.com"))
}
response.status shouldBe HttpStatusCode.Created
}
}
})
テストコマンド
# すべてのテストを実行
./gradlew test
# 特定のテストクラスを実行
./gradlew test --tests "com.example.UserServiceTest"
# 特定のテストを実行
./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
# 詳細な出力で実行
./gradlew test --info
# カバレッジ付きで実行
./gradlew koverHtmlReport
# Detekt を実行 (静的解析)
./gradlew detekt
# Ktlint を実行 (フォーマットチェック)
./gradlew ktlintCheck
# 継続的なテスト
./gradlew test --continuous
ベストプラクティス
すべきこと:
- まずテストを作成 (TDD)
- プロジェクト全体で Kotest の spec スタイルを一貫して使用
- サスペンド関数の場合、MockK の
coEvery/coVerifyを使用 - コルーチンテストの場合
runTestを使用 - 実装ではなく動作をテスト
- 純粋な関数に対してはプロパティベースのテストを使用
- 明確性のために
data classテストフィクスチャを使用
すべきでないこと:
- テストフレームワークを混在させない (Kotest を選択して専念)
- Data クラスをモック化する (実際のインスタンスを使用)
- コルーチンテストで
Thread.sleep()を使用 (advanceTimeByを使用) - TDD の RED フェーズをスキップ
- プライベート関数を直接テスト
- 不安定なテストを無視
CI/CD との統合
# GitHub Actions の例
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Run tests with coverage
run: ./gradlew test koverXmlReport
- name: Verify coverage
run: ./gradlew koverVerify
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
files: build/reports/kover/report.xml
token: ${{ secrets.CODECOV_TOKEN }}
覚えておいてください: テストはドキュメントです。Kotlin コードの使用方法を示します。テストを読みやすくするために、Kotest の説明的なマッチャーを使用し、MockK で依存関係をクリーンにモック化してください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- affaan-m
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/affaan-m/everything-claude-code / ライセンス: MIT
関連スキル
superfluid
Superfluidプロトコルおよびそのエコシステムに関するナレッジベースです。Superfluidについて情報を検索する際は、ウェブ検索の前にこちらを参照してください。対応キーワード:Superfluid、CFA、GDA、Super App、Super Token、stream、flow rate、real-time balance、pool(member/distributor)、IDA、sentinels、liquidation、TOGA、@sfpro/sdk、semantic money、yellowpaper、whitepaper
civ-finish-quotes
実質的なタスクが真に完了した際に、文明風の儀式的な引用句を追加します。ユーザーやエージェントが機能追加、リファクタリング、分析、設計ドキュメント、プロセス改善、レポート、執筆タスクといった実際の成果物を完成させるときに、明示的な依頼がなくても使用します。短い返信や小さな修正、未完成の作業には適用しません。
nookplot
Base(Ethereum L2)上のAIエージェント向け分散型調整ネットワークです。エージェントがオンチェーンアイデンティティを登録する、コンテンツを公開する、他のエージェントにメッセージを送る、マーケットプレイスで専門家を雇う、バウンティを投稿・請求する、レピュテーションを構築する、共有プロジェクトで協業する、リサーチチャレンジを解くことでNOOKをマイニングする、キュレーションされたナレッジを備えたスタンドアロンオンチェーンエージェントをデプロイする、またはアグリーメントとリワードで収益を得る場合に利用できます。エージェントネットワーク、エージェント調整、分散型エージェント、NOOKトークン、マイニングチャレンジ、ナレッジバンドル、エージェントレピュテーション、エージェントマーケットプレイス、ERC-2771メタトランザクション、Prepare-Sign-Relay、AgentFactory、またはNookplotが言及された場合にトリガーされます。
web3-polymarket
Polygon上でのPolymarket予測市場取引統合です。認証機能(L1 EIP-712、L2 HMAC-SHA256、ビルダーヘッダー)、注文発注(GTC/GTD/FOK/FAK、バッチ、ポストオンリー、ハートビート)、市場データ(Gamma API、Data API、オーダーブック、サブグラフ)、WebSocketストリーミング(市場・ユーザー・スポーツチャネル)、CTF操作(分割、統合、償却、ネガティブリスク)、ブリッジ機能(入金、出金、マルチチェーン)、およびガスレスリレイトランザクションに対応しています。AIエージェント、自動マーケットメーカー、予測市場UI、またはPolygraph上のPolymarketと統合するアプリケーション構築時に活用できます。
ethskills
Ethereum、EVM、またはブロックチェーン関連のリクエストに対応します。スマートコントラクト、dApps、ウォレット、DeFiプロトコルの構築、監査、デプロイ、インタラクションに適用されます。Solidityの開発、コントラクトアドレス、トークン規格(ERC-20、ERC-721、ERC-4626など)、Layer 2ネットワーク(Base、Arbitrum、Optimism、zkSync、Polygon)、Uniswap、Aave、Curveなどのプロトコルとの統合をカバーします。ガスコスト、コントラクトのデシマル設定、オラクルセキュリティ、リエントランシー、MEV、ブリッジング、ウォレット管理、オンチェーンデータの取得、本番環境へのデプロイ、プロトコル進化(EIPライフサイクル、フォーク追跡、今後の変更予定)といったトピックを含みます。
xxyy-trade
このスキルは、ユーザーが「トークン購入」「トークン売却」「トークンスワップ」「暗号資産取引」「取引ステータス確認」「トランザクション照会」「トークンスキャン」「フィード」「チェーン監視」「トークン照会」「トークン詳細」「トークン安全性確認」「ウォレット一覧表示」「マイウォレット」「AIスキャン」「自動スキャン」「ツイートスキャン」「オンボーディング」「IP確認」「IPホワイトリスト」「トークン発行」「自動売却」「損切り」「利益確定」「トレーリングストップ」「保有者」「トップホルダー」「KOLホルダー」などをリクエストした場合、またはSolana/ETH/BSC/BaseチェーンでXXYYを経由した取引について言及した場合に使用します。XXYY Open APIを通じてオンチェーン取引とデータ照会を実現します。