Communitygithub.com

kotlin-testing

Kotlin テストパターン集(Kotest、MockK、コルーチンテスト、プロパティベーステスト、Koverカバレッジ)。TDD手法に従い、慣用的なKotlinプラクティスを使用。KotestやMockKを使ったKotlinテスト作成時、またはコルーチンテストやカバレッジ確認時に使用。

kotlin-testing とは?

このスキルは、Kotlin プロジェクトにおけるテストのための包括的なパターン集を提供します。主な対象は Kotest と MockK であり、TDD(テスト駆動開発)の RED/GREEN/REFACTOR サイクルに沿って、信頼性が高くメンテナンスしやすいテストを作成することを支援します。StringSpec、FunSpec、BehaviorSpec、DescribeSpec といった様々な Kotest スペックスタイルの例、MockK を用いたモックやスパイの設定、コルーチンや Flow のテスト、さらにプロパティベーステストと Kover によるコードカバレッジ計測までをカバーします。このスキルは Claude Code や Cursor などの AI エージェント向けに設計されており、開発者が「テストを先に書き、その後実装する」という TDD ワークフローを効率的に実行できるよう、具体的なコード例と手順を示します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/kotlin-testing

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Kotlin テストパターン

Kotest と MockK を使用して、TDD 手法に従った信頼性が高く保守しやすいテストを作成するための包括的な Kotlin テストパターン集。

使用するタイミング

  • 新しい Kotlin 関数やクラスを作成するとき
  • 既存の Kotlin コードにテストカバレッジを追加するとき
  • プロパティベーステストを実装するとき
  • Kotlin プロジェクトで TDD ワークフローに従うとき
  • Kover をコードカバレッジ用に設定するとき

仕組み

  1. 対象コードを特定 — テスト対象の関数、クラス、モジュールを見つける
  2. Kotest スペックを記述 — テスト範囲に合ったスペックスタイル(StringSpec、FunSpec、BehaviorSpec)を選択する
  3. 依存関係をモック化 — MockK を使用してテスト対象を分離する
  4. テストを実行(RED) — 期待されるエラーでテストが失敗することを確認する
  5. コードを実装(GREEN) — テストに合格する最小限のコードを記述する
  6. リファクタリング — テストがグリーンのまま実装を改善する
  7. カバレッジを確認 — ./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({
    "有効なメールアドレスは成功を返す" {
        validateEmail("[email protected]").shouldBeSuccess("[email protected]")
    }

    "空のメールアドレスは失敗を返す" {
        validateEmail("").shouldBeFailure()
    }

    "@ がないメールアドレスは失敗を返す" {
        validateEmail("userexample.com").shouldBeFailure()
    }
})

// ステップ 3: テストを実行 - FAIL を確認
// $ ./gradlew test
// EmailValidatorTest > 有効なメールアドレスは成功を返す FAILED
//   kotlin.NotImplementedError: An operation is not implemented

// ステップ 4: 最小限のコードを実装 (GREEN)
fun validateEmail(email: String): Result<String> {
    if (email.isBlank()) return Result.failure(IllegalArgumentException("メールアドレスは空にできません"))
    if ('@' !in email) return Result.failure(IllegalArgumentException("メールアドレスには @ が必要です"))
    val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
    if (!regex.matches(email)) return Result.failure(IllegalArgumentException("メールアドレスの形式が無効です"))
    return Result.success(email)
}

// ステップ 5: テストを実行 - PASS を確認
// $ ./gradlew test
// EmailValidatorTest > 有効なメールアドレスは成功を返す PASSED
// EmailValidatorTest > 空のメールアドレスは失敗を返す PASSED
// EmailValidatorTest > @ がないメールアドレスは失敗を返す PASSED

// ステップ 6: 必要に応じてリファクタリングし、テストがまだパスすることを確認

Kotest スペックスタイル

StringSpec(最もシンプル)

class CalculatorTest : StringSpec({
    "2つの正の数を加算" {
        Calculator.add(2, 3) shouldBe 5
    }

    "負の数を加算" {
        Calculator.add(-1, -2) shouldBe -3
    }

    "ゼロを加算" {
        Calculator.add(0, 5) shouldBe 5
    }
})

FunSpec(JUnit ライク)

class UserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    test("getUser はユーザーが見つかった場合に返す") {
        val expected = User(id = "1", name = "Alice")
        coEvery { repository.findById("1") } returns expected

        val result = service.getUser("1")

        result shouldBe expected
    }

    test("getUser は見つからない場合に例外をスローする") {
        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("有効な注文リクエスト") {
        val request = CreateOrderRequest(
            userId = "user-1",
            items = listOf(OrderItem("product-1", quantity = 2)),
        )

        When("注文が行われる") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Success
            coEvery { repository.save(any()) } answers { firstArg() }

            val result = service.placeOrder(request)

            Then("確認済みの注文を返す") {
                result.status shouldBe OrderStatus.CONFIRMED
            }

            Then("支払いが請求される") {
                coVerify(exactly = 1) { paymentService.charge(any()) }
            }
        }

        When("支払いが失敗する") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined

            Then("PaymentException をスローする") {
                shouldThrow<PaymentException> {
                    service.placeOrder(request)
                }
            }
        }
    }
})

DescribeSpec(RSpec スタイル)

class UserValidatorTest : DescribeSpec({
    describe("validateUser") {
        val validator = UserValidator()

        context("有効な入力の場合") {
            it("通常のユーザーを受け入れる") {
                val user = CreateUserRequest("Alice", "[email protected]")
                validator.validate(user).shouldBeValid()
            }
        }

        context("無効な名前の場合") {
            it("空白の名前を拒否する") {
                val user = CreateUserRequest("", "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }

            it("最大長を超える名前を拒否する") {
                val user = CreateUserRequest("A".repeat(256), "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }
        }
    }
})

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 "年齢は正の値である必要があります"

shouldNotThrow<Exception> {
    validateAge(25)
}

カスタムマッチャー

fun beActiveUser() = object : Matcher<User> {
    override fun test(value: User) = MatcherResult(
        value.isActive && value.lastLogin != null,
        { "ユーザー ${value.id} はアクティブで最終ログインが必要です" },
        { "ユーザー ${value.id} はアクティブであってはなりません" },
    )
}

// 使用例
user should beActiveUser()

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 はリポジトリに委譲する") {
        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 は不明な ID に対して null を返す") {
        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 サスペンド関数") {
        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 に遅延がある場合") {
        coEvery { repository.findById("1") } coAnswers {
            delay(100) // 非同期処理をシミュレート
            User(id = "1", name = "Alice")
        }

        val result = service.getUser("1")
        result.name shouldBe "Alice"
    }
})

引数キャプチャ

test("save はユーザー引数をキャプチャする") {
    val slot = slot<User>()
    coEvery { repository.save(capture(slot)) } returns Unit

    service.createUser(CreateUserRequest("Alice", "[email protected]"))

    slot.captured.name shouldBe "Alice"
    slot.captured.email shouldBe "[email protected]"
    slot.captured.id.shouldNotBeNull()
}

スパイと部分モッキング

test("実際のオブジェクトをスパイする") {
    val realService = UserService(repository)
    val spy = spyk(realService)

    every { spy.generateId() } returns "fixed-id"

    spy.createUser(request)

    verify { spy.generateId() } // オーバーライドされる
    // 他のメソッドは実際の実装を使用
}

コルーチンテスト

サスペンド関数の runTest

import kotlinx.coroutines.test.runTest

class CoroutineServiceTest : FunSpec({
    test("同時フェッチが一緒に完了する") {
        runTest {
            val service = DataService(testScope = this)

            val result = service.fetchAllData()

            result.users.shouldNotBeEmpty()
            result.products.shouldNotBeEmpty()
        }
    }

    test("遅延後にタイムアウト") {
        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 は更新を発行する") {
        runTest {
            val service = UserFlowService()

            val emissions = service.observeUsers()
                .take(3)
                .toList()

            emissions shouldHaveSize 3
            emissions.last().shouldNotBeEmpty()
        }
    }

    test("searchUsers は入力をデバウンスする") {
        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()
        }
    }
})

TestDispatcher

import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle

class DispatcherTest : FunSpec({
    test("制御された実行にテストディスパッチャーを使用") {
        val dispatcher = StandardTestDispatcher()

        runTest(dispatcher) {
            var completed = false

            launch {
                delay(1000)
                completed = true
            }

            completed shouldBe false
            advanceTimeBy(1000)
            completed shouldBe true
        }
    }
})

プロパティベーステスト

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
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString

// 注: 以下のシリアライゼーションラウンドトリップテストでは、User データクラスに
// kotlinx.serialization の @Serializable アノテーションが必要です。

class PropertyTest : FunSpec({
    test("文字列の反転は対合操作である") {
        forAll<String> { s ->
            s.reversed().reversed() == s
        }
    }

    test("リストのソートは冪等である") {
        forAll(Arb.list(Arb.int())) { list ->
            list.sorted() == list.sorted().sorted()
        }
    }

    test("シリアライゼーションのラウンドトリップはデータを保持する") {
        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
        }
    }
})

カスタムジェネレーター

import io.kotest.property.Arb
import io.kotest.property.arbitrary.*

// 有効なユーザー名のジェネレーター
val validUsernameArb = Arb.string(3..20, Codepoint.alphanumeric()).filter { it.isNotBlank() }

// 特定のドメインのメールアドレスのジェネレーター
val corporateEmailArb = Arb.bind(
    Arb.string(5..15, Codepoint.alphanumeric()),
    Arb.string(4..8, Codepoint.alphanumeric()),
) { local, domain -> "${local.lowercase()}@${domain.lowercase()}.com" }

// 使用例
test("カスタムジェネレーターでプロパティテスト") {
    forAll(validUsernameArb) { username ->
        username.length in 3..20 && username.all { it.isLetterOrDigit() }
    }
}

Kover カバレッジ

設定

// build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.jvm") version "1.9.0"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0"
    id("org.jetbrains.kotlinx.kover") version "0.7.6"
}

koverReport {
    defaults {
        html {
            onCheck = true
        }
    }
}

dependencies {
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest:kotest-assertions-core:5.8.0")
    testImplementation("io.kotest:kotest-property:5.8.0")
    testImplementation("io.mockk:mockk:1.13.10")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}

コマンド

# すべてのテストを実行
./gradlew test

# HTML カバレッジレポートを生成
./gradlew koverHtmlReport

# XML カバレッジレポートを生成(CI 向け)
./gradlew koverXmlReport

# ベリフィケーションルール付きでレポートを生成
./gradlew koverVerify

# レポート場所:
# build/reports/kover/html/
# build/reports/kover/xml/

カバレッジルールの検証

// build.gradle.kts
koverReport {
    verify {
        rule {
            minBound(
                lineCountPercentage = 80.0
            )
        }
    }
}

Ktor testApplication テスト

サーバーテストのセットアップ

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*

class UserRoutesTest : FunSpec({
    test("GET /users はすべてのユーザーを返す") {
        testApplication {
            application {
                module()
            }

            val response = client.get("/users")

            response.status shouldBe HttpStatusCode.OK
            response.bodyAsText() shouldContain "Alice"
            response.bodyAsText() shouldContain "Bob"
        }
    }

    test("GET /users/{id} は特定のユーザーを返す") {
        testApplication {
            application {
                module()
            }

            val response = client.get("/users/1")

            response.status shouldBe HttpStatusCode.OK
            response.bodyAsText() shouldContain "Alice"
        }
    }

    test("GET /users/{id} は不明な ID に対して 404 を返す") {
        testApplication {
            application {
                module()
            }

            val response = client.get("/users/999")

            response.status shouldBe HttpStatusCode.NotFound
        }
    }
})

認証済みリクエストのテスト

class AuthenticatedRoutesTest : FunSpec({
    test("認証されたユーザーは保護されたルートにアクセスできる") {
        testApplication {
            application {
                module()
            }

            val response = client.get("/protected/profile") {
                bearerAuth("valid-token")
            }

            response.status shouldBe HttpStatusCode.OK
        }
    }

    test("認証されていないリクエストは拒否される") {
        testApplication {
            application {
                module()
            }

            val response = client.get("/protected/profile")

            response.status shouldBe HttpStatusCode.Unauthorized
        }
    }
})

ベストプラクティス

命名規則

  1. テスト名 — 機能/振る舞いを説明する説明的な名前を使用する

    // 良い例
    "空のメールアドレスは失敗を返す"
    
    // 悪い例
    "test1"
    
  2. Given-When-Then — BehaviorSpec または DescribeSpec で BDD 命名を使用する

テストの分離

  1. テストは独立しており、順序に依存しないようにする
  2. 各テストの前に clearMocks を使用してモックをリセットする
  3. 共有の可変状態を避けるために、テスト間で新しいインスタンスを作成する

アサーション

  1. 簡潔な等価性チェックには shouldBe を使用する
  2. より複雑なチェックには専用マッチャー(コレクション、文字列、null)を使用する
  3. 必要な場合にのみカスタムマッチャーを作成する

モッキング

  1. ハッピーパスとエラーケースの両方をモックする
  2. 各テストでモックの相互作用を検証する
  3. コルーチン関数には coEvery / coVerify を使用する

コードカバレッジ

  1. 重要なコードには 80% 以上のカバレッジを目指す
  2. カバレッジのレポートは CI パイプラインで自動化する
  3. カバレッジ検証ルールを使用して標準を強制する

Individual skills in this repo

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

accessibility

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

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

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

agent-eval

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

agent-harness-construction

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

agentic-engineering

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

agentic-os

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

agent-introspection-debugging

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

agent-payment-x402

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

関連スキル