Communitygithub.com

kotlin-testing

Kotlin 테스트 패턴: Kotest, MockK, 코루틴 테스트, 속성 기반 테스트, Kover 커버리지. TDD 방법론과 관용적인 Kotlin 관행을 따릅니다. Kotest나 MockK를 사용한 Kotlin 테스트 작성, 코루틴 테스트, 커버리지 확인 시 사용하세요.

kotlin-testing란 무엇인가요?

이 스킬은 Kotlin 프로젝트에서 신뢰할 수 있고 유지 관리 가능한 테스트를 작성하기 위한 포괄적인 패턴을 제공합니다. Kotest, MockK, Kover와 같은 최신 Kotlin 테스트 도구를 사용하여 TDD(테스트 주도 개발) 방법론을 따릅니다. 주요 GitHub Copilot 에이전트, Cursor, Codex, Claude Code 등과 호환되며, 개발자가 Kotest 사양 스타일(StringSpec, FunSpec, BehaviorSpec, DescribeSpec)을 선택하고 MockK를 사용한 의존성 모의, 코루틴 테스트, 속성 기반 테스트, 코드 커버리지 분석을 쉽게 수행할 수 있도록 지원합니다. 각 패턴은 상세한 예제와 함께 제공되어 RED/GREEN/REFACTOR 주기를 명확히 이해하고 실습할 수 있습니다.

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

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

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

문서

Kotlin 테스트 패턴

Kotest와 MockK를 사용하여 TDD 방법론을 따르는 신뢰할 수 있고 유지 관리 가능한 테스트 작성을 위한 포괄적인 Kotlin 테스트 패턴입니다.

사용 시기

  • 새 Kotlin 함수나 클래스를 작성할 때
  • 기존 Kotlin 코드에 테스트 커버리지를 추가할 때
  • 속성 기반 테스트를 구현할 때
  • Kotlin 프로젝트에서 TDD 워크플로를 따를 때
  • Kover를 사용한 코드 커버리지 설정 시

작동 방식

  1. 대상 코드 식별 — 테스트할 함수, 클래스 또는 모듈을 찾습니다.
  2. Kotest 사양 작성 — 테스트 범위에 맞는 사양 스타일(StringSpec, FunSpec, BehaviorSpec)을 선택합니다.
  3. 의존성 모의(Mock) — 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단계: 테스트 실행 - 실패 확인
// $ ./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단계: 테스트 실행 - 통과 확인
// $ ./gradlew test
// EmailValidatorTest > 유효한 이메일은 성공 반환 PASSED
// EmailValidatorTest > 빈 이메일은 실패 반환 PASSED
// EmailValidatorTest > @가 없는 이메일은 실패 반환 PASSED

// 6단계: 필요 시 리팩터링 후 테스트가 여전히 통과하는지 확인

Kotest 사양 스타일

StringSpec (가장 간단함)

class CalculatorTest : StringSpec({
    "두 양수 더하기" {
        Calculator.add(2, 3) shouldBe 5
    }

    "음수 더하기" {
        Calculator.add(-1, -2) shouldBe -3
    }

    "0 더하기" {
        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가 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가 알 수 없는 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 데이터 클래스가
// @Serializable (kotlinx.serialization에서) 어노테이션되어 있어야 합니다.

class PropertyTest : FunSpec({
    test("문자열 뒤집기는 멱등(involutory)이다") {
        forAll<String> { s ->
            s.reversed().reversed() == s
        }
    }

    test("리스트 정렬은 멱등(idempotent)이다") {
        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
        }
    }
})

사용자 정의 생성기

class CustomGeneratorTest : FunSpec({
    test("사용자 정의 생성기로 이메일 테스트") {
        val emailArb = Arb.string(5..30, Arb.Companion.stringVal().let {
            Arb.string(5..30) // 임시: 유효한 이메일 생성을 위한 실제 구현 필요
        })

        forAll<String> { email ->
            val atCount = email.count { it == '@' }
            atCount <= 1
        }
    }
})

Kover 커버리지

Gradle 설정

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

dependencies {
    testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
    testImplementation("io.kotest:kotest-assertions-core:5.9.1")
    testImplementation("io.kotest:kotest-property:5.9.1")
    testImplementation("io.mockk:mockk:1.13.12")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}

// Kover 설정 (선택 사항)
kover {
    engine.set("intellij") // 또는 "jacoco"
}

커버리지 명령어

# HTML 보고서 생성
./gradlew koverHtmlReport
# 보고서 위치: build/reports/kover/html/index.html

# XML 보고서 (CI용)
./gradlew koverXmlReport
# 보고서 위치: build/reports/kover/xml/report.xml

# 검증 (기본: 80% 라인 커버리지)
./gradlew koverVerify

# 특정 규칙으로 검증
# build.gradle.kts:
# kover {
#     verify {
#         rule {
#             minOf(80) // 최소 라인 커버리지 80%
#         }
#     }
# }

커버리지 제외

// build.gradle.kts에서 특정 클래스 제외
kover {
    excludeClassses {
        classes(
            "com.example.generated.*",
            "com.example.MainKt*",
        )
    }
}

// 코드에서 제외
@Suppress("UNUSED")
class UtilityClass {
    // Kover가 이 클래스를 무시하도록 함
}

Ktor testApplication 테스트

testApplication 설정

class UserApiTest : FunSpec({
    test("GET /users/{id}가 사용자를 반환") {
        testApplication {
            application {
                configureRouting()
            }

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

            response.status shouldBe HttpStatusCode.OK
            response.bodyAsText() shouldContain "\"id\":\"1\""
        }
    }

    test("POST /users가 사용자 생성") {
        testApplication {
            application {
                configureRouting()
            }

            val newUser = CreateUserRequest("Bob", "[email protected]")
            val response = client.post("/users") {
                contentType(ContentType.Application.Json)
                setBody(newUser)
            }

            response.status shouldBe HttpStatusCode.Created
        }
    }
})

모범 사례

일반 지침

  1. 의미 있는 테스트 이름 사용 — 테스트 이름은 동작을 설명해야 함

    • 좋은 예: "빈 이메일 주소로 로그인하면 예외가 발생"
    • 나쁜 예: "testLoginFails"
  2. AAA 패턴 따르기 — Arrange, Act, Assert

    test("사용자 생성") {
        // Arrange
        val request = CreateUserRequest("Alice", "[email protected]")
        every { repository.save(any()) } returns User("1", "Alice", "[email protected]")
    
        // Act
        val user = service.createUser(request)
    
        // Assert
        user.id.shouldNotBeNull()
        user.name shouldBe "Alice"
    }
    
  3. 테스트 격리 — 각 테스트는 독립적이어야 함

    • beforeTest/afterTest에서 모의 객체 초기화
    • 공유 가변 상태 피하기
  4. 모의는 적게 — 필요한 경우에만 모의 사용

    • 값 객체나 순수 함수는 모의하지 않음
    • 외부 의존성(데이터베이스, API)에 모의 사용
  5. 모든 경로 테스트 — 성공 케이스, 오류 케이스, 경계 케이스

    • 경계 케이스: 빈 입력, null, 최대 길이, 특수 문자
    • 오류 케이스: 예외, 타임아웃, 거부

테스트 구조

src/
├── main/kotlin/
│   └── com/example/
│       ├── validator/
│       │   └── EmailValidator.kt
│       └── service/
│           └── UserService.kt
└── test/kotlin/
    └── com/example/
        ├── validator/
        │   └── EmailValidatorTest.kt
        └── service/
            └── UserServiceTest.kt

참고: 표준 Gradle 프로젝트 구조 유지 — 테스트는 src/test/kotlin/ 아래에 있고, 프로덕션 코드와 동일한 패키지를 사용합니다.

TDD 체크리스트

  • 구현 전에 실패하는 테스트를 먼저 작성했습니까?
  • 테스트가 프로덕션 코드 변경 없이 독립적으로 실행됩니까?
  • 모의는 필요한 의존성에만 사용되었습니까?
  • 코루틴 코드에서 runTest를 사용했습니까?
  • 엣지 케이스(비어 있음, null, 특수 문자)를 테스트했습니까?
  • 커버리지가 80% 이상입니까?
  • 속성 기반 테스트가 주요 동작을 검증합니까?
  • 각 테스트는 단일 책임을 가지며, 한 가지를 명확히 테스트합니까?

Individual skills in this repo

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

accessibility

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

affaan-m/content-engine

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

affaan-m/ECC

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

affaan-m/fal-ai-media

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

affaan-m/manim-video

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

affaan-m/remotion-video-creation

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

affaan-m/video-editing

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

agent-architecture-audit

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

agent-eval

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

agent-harness-construction

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

agentic-engineering

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

agentic-os

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

agent-introspection-debugging

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

agent-payment-x402

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

agent-self-evaluation

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

agent-sort

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

ai-first-engineering

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

ai-regression-testing

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

android-clean-architecture

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

angular-developer

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

관련 스킬