[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JV52DXDSR2BVWWR978EPZW":3,"skill-same:01M6JV52DXDSR2BVWWR978EPZW":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":34,"linkOnly":35,"markdownContent":36,"markdownBody":37,"frontMatter":38,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":43,"repositoryHtmlUrl":44,"repositoryHomepageUrl":45,"ownerHtmlUrl":46,"ownerBlogUrl":47,"ownerDescription":48,"updatedAt":49,"install":50,"resources":63},"01M6JV52DXDSR2BVWWR978EPZW","kotlin-testing",null,"Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.","hashgraph-online\u002Fawesome-codex-plugins","hashgraph-online","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F160371962?v=4","01M6CNB6NRJVNNGN14TR01Q6D3","HOL (Hashgraph Online)","01M6JT1HSKKG9V68KQAQGBA7XP",1571,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002FColin4k1024\u002Ftsp\u002Fskills\u002Fkotlin-testing\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33],"testing","kotlin testing","kotlin","tdd","methodology","testing patterns","coverage","patterns","with","idiomatic","2026-11-10 10:12:30",false,"---\nname: kotlin-testing\ndescription: Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.\norigin: ECC\n---\n\n# Kotlin Testing Patterns\n\nComprehensive Kotlin testing patterns for writing reliable, maintainable tests following TDD methodology with Kotest and MockK.\n\n## When to Use\n\n- Writing new Kotlin functions or classes\n- Adding test coverage to existing Kotlin code\n- Implementing property-based tests\n- Following TDD workflow in Kotlin projects\n- Configuring Kover for code coverage\n\n## How It Works\n\n1. **Identify target code** — Find the function, class, or module to test\n2. **Write a Kotest spec** — Choose a spec style (StringSpec, FunSpec, BehaviorSpec) matching the test scope\n3. **Mock dependencies** — Use MockK to isolate the unit under test\n4. **Run tests (RED)** — Verify the test fails with the expected error\n5. **Implement code (GREEN)** — Write minimal code to pass the test\n6. **Refactor** — Improve the implementation while keeping tests green\n7. **Check coverage** — Run `.\u002Fgradlew koverHtmlReport` and verify 80%+ coverage\n\n## Examples\n\nThe following sections contain detailed, runnable examples for each testing pattern:\n\n### Quick Reference\n\n- **Kotest specs** — StringSpec, FunSpec, BehaviorSpec, DescribeSpec examples in [Kotest Spec Styles](#kotest-spec-styles)\n- **Mocking** — MockK setup, coroutine mocking, argument capture in [MockK](#mockk)\n- **TDD walkthrough** — Full RED\u002FGREEN\u002FREFACTOR cycle with EmailValidator in [TDD Workflow for Kotlin](#tdd-workflow-for-kotlin)\n- **Coverage** — Kover configuration and commands in [Kover Coverage](#kover-coverage)\n- **Ktor testing** — testApplication setup in [Ktor testApplication Testing](#ktor-testapplication-testing)\n\n### TDD Workflow for Kotlin\n\n#### The RED-GREEN-REFACTOR Cycle\n\n```\nRED     -> Write a failing test first\nGREEN   -> Write minimal code to pass the test\nREFACTOR -> Improve code while keeping tests green\nREPEAT  -> Continue with next requirement\n```\n\n#### Step-by-Step TDD in Kotlin\n\n```kotlin\n\u002F\u002F Step 1: Define the interface\u002Fsignature\n\u002F\u002F EmailValidator.kt\npackage com.example.validator\n\nfun validateEmail(email: String): Result\u003CString> {\n    TODO(\"not implemented\")\n}\n\n\u002F\u002F Step 2: Write failing test (RED)\n\u002F\u002F EmailValidatorTest.kt\npackage com.example.validator\n\nimport io.kotest.core.spec.style.StringSpec\nimport io.kotest.matchers.result.shouldBeFailure\nimport io.kotest.matchers.result.shouldBeSuccess\n\nclass EmailValidatorTest : StringSpec({\n    \"valid email returns success\" {\n        validateEmail(\"user@example.com\").shouldBeSuccess(\"user@example.com\")\n    }\n\n    \"empty email returns failure\" {\n        validateEmail(\"\").shouldBeFailure()\n    }\n\n    \"email without @ returns failure\" {\n        validateEmail(\"userexample.com\").shouldBeFailure()\n    }\n})\n\n\u002F\u002F Step 3: Run tests - verify FAIL\n\u002F\u002F $ .\u002Fgradlew test\n\u002F\u002F EmailValidatorTest > valid email returns success FAILED\n\u002F\u002F   kotlin.NotImplementedError: An operation is not implemented\n\n\u002F\u002F Step 4: Implement minimal code (GREEN)\nfun validateEmail(email: String): Result\u003CString> {\n    if (email.isBlank()) return Result.failure(IllegalArgumentException(\"Email cannot be blank\"))\n    if ('@' !in email) return Result.failure(IllegalArgumentException(\"Email must contain @\"))\n    val regex = Regex(\"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\\.[A-Za-z]{2,}$\")\n    if (!regex.matches(email)) return Result.failure(IllegalArgumentException(\"Invalid email format\"))\n    return Result.success(email)\n}\n\n\u002F\u002F Step 5: Run tests - verify PASS\n\u002F\u002F $ .\u002Fgradlew test\n\u002F\u002F EmailValidatorTest > valid email returns success PASSED\n\u002F\u002F EmailValidatorTest > empty email returns failure PASSED\n\u002F\u002F EmailValidatorTest > email without @ returns failure PASSED\n\n\u002F\u002F Step 6: Refactor if needed, verify tests still pass\n```\n\n### Kotest Spec Styles\n\n#### StringSpec (Simplest)\n\n```kotlin\nclass CalculatorTest : StringSpec({\n    \"add two positive numbers\" {\n        Calculator.add(2, 3) shouldBe 5\n    }\n\n    \"add negative numbers\" {\n        Calculator.add(-1, -2) shouldBe -3\n    }\n\n    \"add zero\" {\n        Calculator.add(0, 5) shouldBe 5\n    }\n})\n```\n\n#### FunSpec (JUnit-like)\n\n```kotlin\nclass UserServiceTest : FunSpec({\n    val repository = mockk\u003CUserRepository>()\n    val service = UserService(repository)\n\n    test(\"getUser returns user when found\") {\n        val expected = User(id = \"1\", name = \"Alice\")\n        coEvery { repository.findById(\"1\") } returns expected\n\n        val result = service.getUser(\"1\")\n\n        result shouldBe expected\n    }\n\n    test(\"getUser throws when not found\") {\n        coEvery { repository.findById(\"999\") } returns null\n\n        shouldThrow\u003CUserNotFoundException> {\n            service.getUser(\"999\")\n        }\n    }\n})\n```\n\n#### BehaviorSpec (BDD Style)\n\n```kotlin\nclass OrderServiceTest : BehaviorSpec({\n    val repository = mockk\u003COrderRepository>()\n    val paymentService = mockk\u003CPaymentService>()\n    val service = OrderService(repository, paymentService)\n\n    Given(\"a valid order request\") {\n        val request = CreateOrderRequest(\n            userId = \"user-1\",\n            items = listOf(OrderItem(\"product-1\", quantity = 2)),\n        )\n\n        When(\"the order is placed\") {\n            coEvery { paymentService.charge(any()) } returns PaymentResult.Success\n            coEvery { repository.save(any()) } answers { firstArg() }\n\n            val result = service.placeOrder(request)\n\n            Then(\"it should return a confirmed order\") {\n                result.status shouldBe OrderStatus.CONFIRMED\n            }\n\n            Then(\"it should charge payment\") {\n                coVerify(exactly = 1) { paymentService.charge(any()) }\n            }\n        }\n\n        When(\"payment fails\") {\n            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined\n\n            Then(\"it should throw PaymentException\") {\n                shouldThrow\u003CPaymentException> {\n                    service.placeOrder(request)\n                }\n            }\n        }\n    }\n})\n```\n\n#### DescribeSpec (RSpec Style)\n\n```kotlin\nclass UserValidatorTest : DescribeSpec({\n    describe(\"validateUser\") {\n        val validator = UserValidator()\n\n        context(\"with valid input\") {\n            it(\"accepts a normal user\") {\n                val user = CreateUserRequest(\"Alice\", \"alice@example.com\")\n                validator.validate(user).shouldBeValid()\n            }\n        }\n\n        context(\"with invalid name\") {\n            it(\"rejects blank name\") {\n                val user = CreateUserRequest(\"\", \"alice@example.com\")\n                validator.validate(user).shouldBeInvalid()\n            }\n\n            it(\"rejects name exceeding max length\") {\n                val user = CreateUserRequest(\"A\".repeat(256), \"alice@example.com\")\n                validator.validate(user).shouldBeInvalid()\n            }\n        }\n    }\n})\n```\n\n### Kotest Matchers\n\n#### Core Matchers\n\n```kotlin\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.*\nimport io.kotest.matchers.collections.*\nimport io.kotest.matchers.nulls.*\n\n\u002F\u002F Equality\nresult shouldBe expected\nresult shouldNotBe unexpected\n\n\u002F\u002F Strings\nname shouldStartWith \"Al\"\nname shouldEndWith \"ice\"\nname shouldContain \"lic\"\nname shouldMatch Regex(\"[A-Z][a-z]+\")\nname.shouldBeBlank()\n\n\u002F\u002F Collections\nlist shouldContain \"item\"\nlist shouldHaveSize 3\nlist.shouldBeSorted()\nlist.shouldContainAll(\"a\", \"b\", \"c\")\nlist.shouldBeEmpty()\n\n\u002F\u002F Nulls\nresult.shouldNotBeNull()\nresult.shouldBeNull()\n\n\u002F\u002F Types\nresult.shouldBeInstanceOf\u003CUser>()\n\n\u002F\u002F Numbers\ncount shouldBeGreaterThan 0\nprice shouldBeInRange 1.0..100.0\n\n\u002F\u002F Exceptions\nshouldThrow\u003CIllegalArgumentException> {\n    validateAge(-1)\n}.message shouldBe \"Age must be positive\"\n\nshouldNotThrow\u003CException> {\n    validateAge(25)\n}\n```\n\n#### Custom Matchers\n\n```kotlin\nfun beActiveUser() = object : Matcher\u003CUser> {\n    override fun test(value: User) = MatcherResult(\n        value.isActive && value.lastLogin != null,\n        { \"User ${value.id} should be active with a last login\" },\n        { \"User ${value.id} should not be active\" },\n    )\n}\n\n\u002F\u002F Usage\nuser should beActiveUser()\n```\n\n### MockK\n\n#### Basic Mocking\n\n```kotlin\nclass UserServiceTest : FunSpec({\n    val repository = mockk\u003CUserRepository>()\n    val logger = mockk\u003CLogger>(relaxed = true) \u002F\u002F Relaxed: returns defaults\n    val service = UserService(repository, logger)\n\n    beforeTest {\n        clearMocks(repository, logger)\n    }\n\n    test(\"findUser delegates to repository\") {\n        val expected = User(id = \"1\", name = \"Alice\")\n        every { repository.findById(\"1\") } returns expected\n\n        val result = service.findUser(\"1\")\n\n        result shouldBe expected\n        verify(exactly = 1) { repository.findById(\"1\") }\n    }\n\n    test(\"findUser returns null for unknown id\") {\n        every { repository.findById(any()) } returns null\n\n        val result = service.findUser(\"unknown\")\n\n        result.shouldBeNull()\n    }\n})\n```\n\n#### Coroutine Mocking\n\n```kotlin\nclass AsyncUserServiceTest : FunSpec({\n    val repository = mockk\u003CUserRepository>()\n    val service = UserService(repository)\n\n    test(\"getUser suspending function\") {\n        coEvery { repository.findById(\"1\") } returns User(id = \"1\", name = \"Alice\")\n\n        val result = service.getUser(\"1\")\n\n        result.name shouldBe \"Alice\"\n        coVerify { repository.findById(\"1\") }\n    }\n\n    test(\"getUser with delay\") {\n        coEvery { repository.findById(\"1\") } coAnswers {\n            delay(100) \u002F\u002F Simulate async work\n            User(id = \"1\", name = \"Alice\")\n        }\n\n        val result = service.getUser(\"1\")\n        result.name shouldBe \"Alice\"\n    }\n})\n```\n\n#### Argument Capture\n\n```kotlin\ntest(\"save captures the user argument\") {\n    val slot = slot\u003CUser>()\n    coEvery { repository.save(capture(slot)) } returns Unit\n\n    service.createUser(CreateUserRequest(\"Alice\", \"alice@example.com\"))\n\n    slot.captured.name shouldBe \"Alice\"\n    slot.captured.email shouldBe \"alice@example.com\"\n    slot.captured.id.shouldNotBeNull()\n}\n```\n\n#### Spy and Partial Mocking\n\n```kotlin\ntest(\"spy on real object\") {\n    val realService = UserService(repository)\n    val spy = spyk(realService)\n\n    every { spy.generateId() } returns \"fixed-id\"\n\n    spy.createUser(request)\n\n    verify { spy.generateId() } \u002F\u002F Overridden\n    \u002F\u002F Other methods use real implementation\n}\n```\n\n### Coroutine Testing\n\n#### runTest for Suspend Functions\n\n```kotlin\nimport kotlinx.coroutines.test.runTest\n\nclass CoroutineServiceTest : FunSpec({\n    test(\"concurrent fetches complete together\") {\n        runTest {\n            val service = DataService(testScope = this)\n\n            val result = service.fetchAllData()\n\n            result.users.shouldNotBeEmpty()\n            result.products.shouldNotBeEmpty()\n        }\n    }\n\n    test(\"timeout after delay\") {\n        runTest {\n            val service = SlowService()\n\n            shouldThrow\u003CTimeoutCancellationException> {\n                withTimeout(100) {\n                    service.slowOperation() \u002F\u002F Takes > 100ms\n                }\n            }\n        }\n    }\n})\n```\n\n#### Testing Flows\n\n```kotlin\nimport io.kotest.matchers.collections.shouldContainInOrder\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.toList\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.advanceTimeBy\nimport kotlinx.coroutines.test.runTest\n\nclass FlowServiceTest : FunSpec({\n    test(\"observeUsers emits updates\") {\n        runTest {\n            val service = UserFlowService()\n\n            val emissions = service.observeUsers()\n                .take(3)\n                .toList()\n\n            emissions shouldHaveSize 3\n            emissions.last().shouldNotBeEmpty()\n        }\n    }\n\n    test(\"searchUsers debounces input\") {\n        runTest {\n            val service = SearchService()\n            val queries = MutableSharedFlow\u003CString>()\n\n            val results = mutableListOf\u003CList\u003CUser>>()\n            val job = launch {\n                service.searchUsers(queries).collect { results.add(it) }\n            }\n\n            queries.emit(\"a\")\n            queries.emit(\"ab\")\n            queries.emit(\"abc\") \u002F\u002F Only this should trigger search\n            advanceTimeBy(500)\n\n            results shouldHaveSize 1\n            job.cancel()\n        }\n    }\n})\n```\n\n#### TestDispatcher\n\n```kotlin\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.advanceUntilIdle\n\nclass DispatcherTest : FunSpec({\n    test(\"uses test dispatcher for controlled execution\") {\n        val dispatcher = StandardTestDispatcher()\n\n        runTest(dispatcher) {\n            var completed = false\n\n            launch {\n                delay(1000)\n                completed = true\n            }\n\n            completed shouldBe false\n            advanceTimeBy(1000)\n            completed shouldBe true\n        }\n    }\n})\n```\n\n### Property-Based Testing\n\n#### Kotest Property Testing\n\n```kotlin\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.property.Arb\nimport io.kotest.property.arbitrary.*\nimport io.kotest.property.forAll\nimport io.kotest.property.checkAll\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.decodeFromString\n\n\u002F\u002F Note: The serialization roundtrip test below requires the User data class\n\u002F\u002F to be annotated with @Serializable (from kotlinx.serialization).\n\nclass PropertyTest : FunSpec({\n    test(\"string reverse is involutory\") {\n        forAll\u003CString> { s ->\n            s.reversed().reversed() == s\n        }\n    }\n\n    test(\"list sort is idempotent\") {\n        forAll(Arb.list(Arb.int())) { list ->\n            list.sorted() == list.sorted().sorted()\n        }\n    }\n\n    test(\"serialization roundtrip preserves data\") {\n        checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->\n            User(name = name, email = \"$email@test.com\")\n        }) { user ->\n            val json = Json.encodeToString(user)\n            val decoded = Json.decodeFromString\u003CUser>(json)\n            decoded shouldBe user\n        }\n    }\n})\n```\n\n#### Custom Generators\n\n```kotlin\nval userArb: Arb\u003CUser> = Arb.bind(\n    Arb.string(minSize = 1, maxSize = 50),\n    Arb.email(),\n    Arb.enum\u003CRole>(),\n) { name, email, role ->\n    User(\n        id = UserId(UUID.randomUUID().toString()),\n        name = name,\n        email = Email(email),\n        role = role,\n    )\n}\n\nval moneyArb: Arb\u003CMoney> = Arb.bind(\n    Arb.long(1L..1_000_000L),\n    Arb.enum\u003CCurrency>(),\n) { amount, currency ->\n    Money(amount, currency)\n}\n```\n\n### Data-Driven Testing\n\n#### withData in Kotest\n\n```kotlin\nclass ParserTest : FunSpec({\n    context(\"parsing valid dates\") {\n        withData(\n            \"2026-01-15\" to LocalDate(2026, 1, 15),\n            \"2026-12-31\" to LocalDate(2026, 12, 31),\n            \"2000-01-01\" to LocalDate(2000, 1, 1),\n        ) { (input, expected) ->\n            parseDate(input) shouldBe expected\n        }\n    }\n\n    context(\"rejecting invalid dates\") {\n        withData(\n            nameFn = { \"rejects '$it'\" },\n            \"not-a-date\",\n            \"2026-13-01\",\n            \"2026-00-15\",\n            \"\",\n        ) { input ->\n            shouldThrow\u003CDateParseException> {\n                parseDate(input)\n            }\n        }\n    }\n})\n```\n\n### Test Lifecycle and Fixtures\n\n#### BeforeTest \u002F AfterTest\n\n```kotlin\nclass DatabaseTest : FunSpec({\n    lateinit var db: Database\n\n    beforeSpec {\n        db = Database.connect(\"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\")\n        transaction(db) {\n            SchemaUtils.create(UsersTable)\n        }\n    }\n\n    afterSpec {\n        transaction(db) {\n            SchemaUtils.drop(UsersTable)\n        }\n    }\n\n    beforeTest {\n        transaction(db) {\n            UsersTable.deleteAll()\n        }\n    }\n\n    test(\"insert and retrieve user\") {\n        transaction(db) {\n            UsersTable.insert {\n                it[name] = \"Alice\"\n                it[email] = \"alice@example.com\"\n            }\n        }\n\n        val users = transaction(db) {\n            UsersTable.selectAll().map { it[UsersTable.name] }\n        }\n\n        users shouldContain \"Alice\"\n    }\n})\n```\n\n#### Kotest Extensions\n\n```kotlin\n\u002F\u002F Reusable test extension\nclass DatabaseExtension : BeforeSpecListener, AfterSpecListener {\n    lateinit var db: Database\n\n    override suspend fun beforeSpec(spec: Spec) {\n        db = Database.connect(\"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\")\n    }\n\n    override suspend fun afterSpec(spec: Spec) {\n        \u002F\u002F cleanup\n    }\n}\n\nclass UserRepositoryTest : FunSpec({\n    val dbExt = DatabaseExtension()\n    register(dbExt)\n\n    test(\"save and find user\") {\n        val repo = UserRepository(dbExt.db)\n        \u002F\u002F ...\n    }\n})\n```\n\n### Kover Coverage\n\n#### Gradle Configuration\n\n```kotlin\n\u002F\u002F build.gradle.kts\nplugins {\n    id(\"org.jetbrains.kotlinx.kover\") version \"0.9.7\"\n}\n\nkover {\n    reports {\n        total {\n            html { onCheck = true }\n            xml { onCheck = true }\n        }\n        filters {\n            excludes {\n                classes(\"*.generated.*\", \"*.config.*\")\n            }\n        }\n        verify {\n            rule {\n                minBound(80) \u002F\u002F Fail build below 80% coverage\n            }\n        }\n    }\n}\n```\n\n#### Coverage Commands\n\n```bash\n# Run tests with coverage\n.\u002Fgradlew koverHtmlReport\n\n# Verify coverage thresholds\n.\u002Fgradlew koverVerify\n\n# XML report for CI\n.\u002Fgradlew koverXmlReport\n\n# View HTML report (use the command for your OS)\n# macOS:   open build\u002Freports\u002Fkover\u002Fhtml\u002Findex.html\n# Linux:   xdg-open build\u002Freports\u002Fkover\u002Fhtml\u002Findex.html\n# Windows: start build\u002Freports\u002Fkover\u002Fhtml\u002Findex.html\n```\n\n#### Coverage Targets\n\n| Code Type | Target |\n|-----------|--------|\n| Critical business logic | 100% |\n| Public APIs | 90%+ |\n| General code | 80%+ |\n| Generated \u002F config code | Exclude |\n\n### Ktor testApplication Testing\n\n```kotlin\nclass ApiRoutesTest : FunSpec({\n    test(\"GET \u002Fusers returns list\") {\n        testApplication {\n            application {\n                configureRouting()\n                configureSerialization()\n            }\n\n            val response = client.get(\"\u002Fusers\")\n\n            response.status shouldBe HttpStatusCode.OK\n            val users = response.body\u003CList\u003CUserResponse>>()\n            users.shouldNotBeEmpty()\n        }\n    }\n\n    test(\"POST \u002Fusers creates user\") {\n        testApplication {\n            application {\n                configureRouting()\n                configureSerialization()\n            }\n\n            val response = client.post(\"\u002Fusers\") {\n                contentType(ContentType.Application.Json)\n                setBody(CreateUserRequest(\"Alice\", \"alice@example.com\"))\n            }\n\n            response.status shouldBe HttpStatusCode.Created\n        }\n    }\n})\n```\n\n### Testing Commands\n\n```bash\n# Run all tests\n.\u002Fgradlew test\n\n# Run specific test class\n.\u002Fgradlew test --tests \"com.example.UserServiceTest\"\n\n# Run specific test\n.\u002Fgradlew test --tests \"com.example.UserServiceTest.getUser returns user when found\"\n\n# Run with verbose output\n.\u002Fgradlew test --info\n\n# Run with coverage\n.\u002Fgradlew koverHtmlReport\n\n# Run detekt (static analysis)\n.\u002Fgradlew detekt\n\n# Run ktlint (formatting check)\n.\u002Fgradlew ktlintCheck\n\n# Continuous testing\n.\u002Fgradlew test --continuous\n```\n\n### Best Practices\n\n**DO:**\n- Write tests FIRST (TDD)\n- Use Kotest's spec styles consistently across the project\n- Use MockK's `coEvery`\u002F`coVerify` for suspend functions\n- Use `runTest` for coroutine testing\n- Test behavior, not implementation\n- Use property-based testing for pure functions\n- Use `data class` test fixtures for clarity\n\n**DON'T:**\n- Mix testing frameworks (pick Kotest and stick with it)\n- Mock data classes (use real instances)\n- Use `Thread.sleep()` in coroutine tests (use `advanceTimeBy`)\n- Skip the RED phase in TDD\n- Test private functions directly\n- Ignore flaky tests\n\n### Integration with CI\u002FCD\n\n```yaml\n# GitHub Actions example\ntest:\n  runs-on: ubuntu-latest\n  steps:\n    - uses: actions\u002Fcheckout@v4\n    - uses: actions\u002Fsetup-java@v4\n      with:\n        distribution: 'temurin'\n        java-version: '21'\n\n    - name: Run tests with coverage\n      run: .\u002Fgradlew test koverXmlReport\n\n    - name: Verify coverage\n      run: .\u002Fgradlew koverVerify\n\n    - name: Upload coverage\n      uses: codecov\u002Fcodecov-action@v5\n      with:\n        files: build\u002Freports\u002Fkover\u002Freport.xml\n        token: ${{ secrets.CODECOV_TOKEN }}\n```\n\n**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.\n","# Kotlin Testing Patterns\n\nComprehensive Kotlin testing patterns for writing reliable, maintainable tests following TDD methodology with Kotest and MockK.\n\n## When to Use\n\n- Writing new Kotlin functions or classes\n- Adding test coverage to existing Kotlin code\n- Implementing property-based tests\n- Following TDD workflow in Kotlin projects\n- Configuring Kover for code coverage\n\n## How It Works\n\n1. **Identify target code** — Find the function, class, or module to test\n2. **Write a Kotest spec** — Choose a spec style (StringSpec, FunSpec, BehaviorSpec) matching the test scope\n3. **Mock dependencies** — Use MockK to isolate the unit under test\n4. **Run tests (RED)** — Verify the test fails with the expected error\n5. **Implement code (GREEN)** — Write minimal code to pass the test\n6. **Refactor** — Improve the implementation while keeping tests green\n7. **Check coverage** — Run `.\u002Fgradlew koverHtmlReport` and verify 80%+ coverage\n\n## Examples\n\nThe following sections contain detailed, runnable examples for each testing pattern:\n\n### Quick Reference\n\n- **Kotest specs** — StringSpec, FunSpec, BehaviorSpec, DescribeSpec examples in [Kotest Spec Styles](#kotest-spec-styles)\n- **Mocking** — MockK setup, coroutine mocking, argument capture in [MockK](#mockk)\n- **TDD walkthrough** — Full RED\u002FGREEN\u002FREFACTOR cycle with EmailValidator in [TDD Workflow for Kotlin](#tdd-workflow-for-kotlin)\n- **Coverage** — Kover configuration and commands in [Kover Coverage](#kover-coverage)\n- **Ktor testing** — testApplication setup in [Ktor testApplication Testing](#ktor-testapplication-testing)\n\n### TDD Workflow for Kotlin\n\n#### The RED-GREEN-REFACTOR Cycle\n\n```\nRED     -> Write a failing test first\nGREEN   -> Write minimal code to pass the test\nREFACTOR -> Improve code while keeping tests green\nREPEAT  -> Continue with next requirement\n```\n\n#### Step-by-Step TDD in Kotlin\n\n```kotlin\n\u002F\u002F Step 1: Define the interface\u002Fsignature\n\u002F\u002F EmailValidator.kt\npackage com.example.validator\n\nfun validateEmail(email: String): Result\u003CString> {\n    TODO(\"not implemented\")\n}\n\n\u002F\u002F Step 2: Write failing test (RED)\n\u002F\u002F EmailValidatorTest.kt\npackage com.example.validator\n\nimport io.kotest.core.spec.style.StringSpec\nimport io.kotest.matchers.result.shouldBeFailure\nimport io.kotest.matchers.result.shouldBeSuccess\n\nclass EmailValidatorTest : StringSpec({\n    \"valid email returns success\" {\n        validateEmail(\"user@example.com\").shouldBeSuccess(\"user@example.com\")\n    }\n\n    \"empty email returns failure\" {\n        validateEmail(\"\").shouldBeFailure()\n    }\n\n    \"email without @ returns failure\" {\n        validateEmail(\"userexample.com\").shouldBeFailure()\n    }\n})\n\n\u002F\u002F Step 3: Run tests - verify FAIL\n\u002F\u002F $ .\u002Fgradlew test\n\u002F\u002F EmailValidatorTest > valid email returns success FAILED\n\u002F\u002F   kotlin.NotImplementedError: An operation is not implemented\n\n\u002F\u002F Step 4: Implement minimal code (GREEN)\nfun validateEmail(email: String): Result\u003CString> {\n    if (email.isBlank()) return Result.failure(IllegalArgumentException(\"Email cannot be blank\"))\n    if ('@' !in email) return Result.failure(IllegalArgumentException(\"Email must contain @\"))\n    val regex = Regex(\"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\\.[A-Za-z]{2,}$\")\n    if (!regex.matches(email)) return Result.failure(IllegalArgumentException(\"Invalid email format\"))\n    return Result.success(email)\n}\n\n\u002F\u002F Step 5: Run tests - verify PASS\n\u002F\u002F $ .\u002Fgradlew test\n\u002F\u002F EmailValidatorTest > valid email returns success PASSED\n\u002F\u002F EmailValidatorTest > empty email returns failure PASSED\n\u002F\u002F EmailValidatorTest > email without @ returns failure PASSED\n\n\u002F\u002F Step 6: Refactor if needed, verify tests still pass\n```\n\n### Kotest Spec Styles\n\n#### StringSpec (Simplest)\n\n```kotlin\nclass CalculatorTest : StringSpec({\n    \"add two positive numbers\" {\n        Calculator.add(2, 3) shouldBe 5\n    }\n\n    \"add negative numbers\" {\n        Calculator.add(-1, -2) shouldBe -3\n    }\n\n    \"add zero\" {\n        Calculator.add(0, 5) shouldBe 5\n    }\n})\n```\n\n#### FunSpec (JUnit-like)\n\n```kotlin\nclass UserServiceTest : FunSpec({\n    val repository = mockk\u003CUserRepository>()\n    val service = UserService(repository)\n\n    test(\"getUser returns user when found\") {\n        val expected = User(id = \"1\", name = \"Alice\")\n        coEvery { repository.findById(\"1\") } returns expected\n\n        val result = service.getUser(\"1\")\n\n        result shouldBe expected\n    }\n\n    test(\"getUser throws when not found\") {\n        coEvery { repository.findById(\"999\") } returns null\n\n        shouldThrow\u003CUserNotFoundException> {\n            service.getUser(\"999\")\n        }\n    }\n})\n```\n\n#### BehaviorSpec (BDD Style)\n\n```kotlin\nclass OrderServiceTest : BehaviorSpec({\n    val repository = mockk\u003COrderRepository>()\n    val paymentService = mockk\u003CPaymentService>()\n    val service = OrderService(repository, paymentService)\n\n    Given(\"a valid order request\") {\n        val request = CreateOrderRequest(\n            userId = \"user-1\",\n            items = listOf(OrderItem(\"product-1\", quantity = 2)),\n        )\n\n        When(\"the order is placed\") {\n            coEvery { paymentService.charge(any()) } returns PaymentResult.Success\n            coEvery { repository.save(any()) } answers { firstArg() }\n\n            val result = service.placeOrder(request)\n\n            Then(\"it should return a confirmed order\") {\n                result.status shouldBe OrderStatus.CONFIRMED\n            }\n\n            Then(\"it should charge payment\") {\n                coVerify(exactly = 1) { paymentService.charge(any()) }\n            }\n        }\n\n        When(\"payment fails\") {\n            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined\n\n            Then(\"it should throw PaymentException\") {\n                shouldThrow\u003CPaymentException> {\n                    service.placeOrder(request)\n                }\n            }\n        }\n    }\n})\n```\n\n#### DescribeSpec (RSpec Style)\n\n```kotlin\nclass UserValidatorTest : DescribeSpec({\n    describe(\"validateUser\") {\n        val validator = UserValidator()\n\n        context(\"with valid input\") {\n            it(\"accepts a normal user\") {\n                val user = CreateUserRequest(\"Alice\", \"alice@example.com\")\n                validator.validate(user).shouldBeValid()\n            }\n        }\n\n        context(\"with invalid name\") {\n            it(\"rejects blank name\") {\n                val user = CreateUserRequest(\"\", \"alice@example.com\")\n                validator.validate(user).shouldBeInvalid()\n            }\n\n            it(\"rejects name exceeding max length\") {\n                val user = CreateUserRequest(\"A\".repeat(256), \"alice@example.com\")\n                validator.validate(user).shouldBeInvalid()\n            }\n        }\n    }\n})\n```\n\n### Kotest Matchers\n\n#### Core Matchers\n\n```kotlin\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.*\nimport io.kotest.matchers.collections.*\nimport io.kotest.matchers.nulls.*\n\n\u002F\u002F Equality\nresult shouldBe expected\nresult shouldNotBe unexpected\n\n\u002F\u002F Strings\nname shouldStartWith \"Al\"\nname shouldEndWith \"ice\"\nname shouldContain \"lic\"\nname shouldMatch Regex(\"[A-Z][a-z]+\")\nname.shouldBeBlank()\n\n\u002F\u002F Collections\nlist shouldContain \"item\"\nlist shouldHaveSize 3\nlist.shouldBeSorted()\nlist.shouldContainAll(\"a\", \"b\", \"c\")\nlist.shouldBeEmpty()\n\n\u002F\u002F Nulls\nresult.shouldNotBeNull()\nresult.shouldBeNull()\n\n\u002F\u002F Types\nresult.shouldBeInstanceOf\u003CUser>()\n\n\u002F\u002F Numbers\ncount shouldBeGreaterThan 0\nprice shouldBeInRange 1.0..100.0\n\n\u002F\u002F Exceptions\nshouldThrow\u003CIllegalArgumentException> {\n    validateAge(-1)\n}.message shouldBe \"Age must be positive\"\n\nshouldNotThrow\u003CException> {\n    validateAge(25)\n}\n```\n\n#### Custom Matchers\n\n```kotlin\nfun beActiveUser() = object : Matcher\u003CUser> {\n    override fun test(value: User) = MatcherResult(\n        value.isActive && value.lastLogin != null,\n        { \"User ${value.id} should be active with a last login\" },\n        { \"User ${value.id} should not be active\" },\n    )\n}\n\n\u002F\u002F Usage\nuser should beActiveUser()\n```\n\n### MockK\n\n#### Basic Mocking\n\n```kotlin\nclass UserServiceTest : FunSpec({\n    val repository = mockk\u003CUserRepository>()\n    val logger = mockk\u003CLogger>(relaxed = true) \u002F\u002F Relaxed: returns defaults\n    val service = UserService(repository, logger)\n\n    beforeTest {\n        clearMocks(repository, logger)\n    }\n\n    test(\"findUser delegates to repository\") {\n        val expected = User(id = \"1\", name = \"Alice\")\n        every { repository.findById(\"1\") } returns expected\n\n        val result = service.findUser(\"1\")\n\n        result shouldBe expected\n        verify(exactly = 1) { repository.findById(\"1\") }\n    }\n\n    test(\"findUser returns null for unknown id\") {\n        every { repository.findById(any()) } returns null\n\n        val result = service.findUser(\"unknown\")\n\n        result.shouldBeNull()\n    }\n})\n```\n\n#### Coroutine Mocking\n\n```kotlin\nclass AsyncUserServiceTest : FunSpec({\n    val repository = mockk\u003CUserRepository>()\n    val service = UserService(repository)\n\n    test(\"getUser suspending function\") {\n        coEvery { repository.findById(\"1\") } returns User(id = \"1\", name = \"Alice\")\n\n        val result = service.getUser(\"1\")\n\n        result.name shouldBe \"Alice\"\n        coVerify { repository.findById(\"1\") }\n    }\n\n    test(\"getUser with delay\") {\n        coEvery { repository.findById(\"1\") } coAnswers {\n            delay(100) \u002F\u002F Simulate async work\n            User(id = \"1\", name = \"Alice\")\n        }\n\n        val result = service.getUser(\"1\")\n        result.name shouldBe \"Alice\"\n    }\n})\n```\n\n#### Argument Capture\n\n```kotlin\ntest(\"save captures the user argument\") {\n    val slot = slot\u003CUser>()\n    coEvery { repository.save(capture(slot)) } returns Unit\n\n    service.createUser(CreateUserRequest(\"Alice\", \"alice@example.com\"))\n\n    slot.captured.name shouldBe \"Alice\"\n    slot.captured.email shouldBe \"alice@example.com\"\n    slot.captured.id.shouldNotBeNull()\n}\n```\n\n#### Spy and Partial Mocking\n\n```kotlin\ntest(\"spy on real object\") {\n    val realService = UserService(repository)\n    val spy = spyk(realService)\n\n    every { spy.generateId() } returns \"fixed-id\"\n\n    spy.createUser(request)\n\n    verify { spy.generateId() } \u002F\u002F Overridden\n    \u002F\u002F Other methods use real implementation\n}\n```\n\n### Coroutine Testing\n\n#### runTest for Suspend Functions\n\n```kotlin\nimport kotlinx.coroutines.test.runTest\n\nclass CoroutineServiceTest : FunSpec({\n    test(\"concurrent fetches complete together\") {\n        runTest {\n            val service = DataService(testScope = this)\n\n            val result = service.fetchAllData()\n\n            result.users.shouldNotBeEmpty()\n            result.products.shouldNotBeEmpty()\n        }\n    }\n\n    test(\"timeout after delay\") {\n        runTest {\n            val service = SlowService()\n\n            shouldThrow\u003CTimeoutCancellationException> {\n                withTimeout(100) {\n                    service.slowOperation() \u002F\u002F Takes > 100ms\n                }\n            }\n        }\n    }\n})\n```\n\n#### Testing Flows\n\n```kotlin\nimport io.kotest.matchers.collections.shouldContainInOrder\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.toList\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.advanceTimeBy\nimport kotlinx.coroutines.test.runTest\n\nclass FlowServiceTest : FunSpec({\n    test(\"observeUsers emits updates\") {\n        runTest {\n            val service = UserFlowService()\n\n            val emissions = service.observeUsers()\n                .take(3)\n                .toList()\n\n            emissions shouldHaveSize 3\n            emissions.last().shouldNotBeEmpty()\n        }\n    }\n\n    test(\"searchUsers debounces input\") {\n        runTest {\n            val service = SearchService()\n            val queries = MutableSharedFlow\u003CString>()\n\n            val results = mutableListOf\u003CList\u003CUser>>()\n            val job = launch {\n                service.searchUsers(queries).collect { results.add(it) }\n            }\n\n            queries.emit(\"a\")\n            queries.emit(\"ab\")\n            queries.emit(\"abc\") \u002F\u002F Only this should trigger search\n            advanceTimeBy(500)\n\n            results shouldHaveSize 1\n            job.cancel()\n        }\n    }\n})\n```\n\n#### TestDispatcher\n\n```kotlin\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.advanceUntilIdle\n\nclass DispatcherTest : FunSpec({\n    test(\"uses test dispatcher for controlled execution\") {\n        val dispatcher = StandardTestDispatcher()\n\n        runTest(dispatcher) {\n            var completed = false\n\n            launch {\n                delay(1000)\n                completed = true\n            }\n\n            completed shouldBe false\n            advanceTimeBy(1000)\n            completed shouldBe true\n        }\n    }\n})\n```\n\n### Property-Based Testing\n\n#### Kotest Property Testing\n\n```kotlin\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.property.Arb\nimport io.kotest.property.arbitrary.*\nimport io.kotest.property.forAll\nimport io.kotest.property.checkAll\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.decodeFromString\n\n\u002F\u002F Note: The serialization roundtrip test below requires the User data class\n\u002F\u002F to be annotated with @Serializable (from kotlinx.serialization).\n\nclass PropertyTest : FunSpec({\n    test(\"string reverse is involutory\") {\n        forAll\u003CString> { s ->\n            s.reversed().reversed() == s\n        }\n    }\n\n    test(\"list sort is idempotent\") {\n        forAll(Arb.list(Arb.int())) { list ->\n            list.sorted() == list.sorted().sorted()\n        }\n    }\n\n    test(\"serialization roundtrip preserves data\") {\n        checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->\n            User(name = name, email = \"$email@test.com\")\n        }) { user ->\n            val json = Json.encodeToString(user)\n            val decoded = Json.decodeFromString\u003CUser>(json)\n            decoded shouldBe user\n        }\n    }\n})\n```\n\n#### Custom Generators\n\n```kotlin\nval userArb: Arb\u003CUser> = Arb.bind(\n    Arb.string(minSize = 1, maxSize = 50),\n    Arb.email(),\n    Arb.enum\u003CRole>(),\n) { name, email, role ->\n    User(\n        id = UserId(UUID.randomUUID().toString()),\n        name = name,\n        email = Email(email),\n        role = role,\n    )\n}\n\nval moneyArb: Arb\u003CMoney> = Arb.bind(\n    Arb.long(1L..1_000_000L),\n    Arb.enum\u003CCurrency>(),\n) { amount, currency ->\n    Money(amount, currency)\n}\n```\n\n### Data-Driven Testing\n\n#### withData in Kotest\n\n```kotlin\nclass ParserTest : FunSpec({\n    context(\"parsing valid dates\") {\n        withData(\n            \"2026-01-15\" to LocalDate(2026, 1, 15),\n            \"2026-12-31\" to LocalDate(2026, 12, 31),\n            \"2000-01-01\" to LocalDate(2000, 1, 1),\n        ) { (input, expected) ->\n            parseDate(input) shouldBe expected\n        }\n    }\n\n    context(\"rejecting invalid dates\") {\n        withData(\n            nameFn = { \"rejects '$it'\" },\n            \"not-a-date\",\n            \"2026-13-01\",\n            \"2026-00-15\",\n            \"\",\n        ) { input ->\n            shouldThrow\u003CDateParseException> {\n                parseDate(input)\n            }\n        }\n    }\n})\n```\n\n### Test Lifecycle and Fixtures\n\n#### BeforeTest \u002F AfterTest\n\n```kotlin\nclass DatabaseTest : FunSpec({\n    lateinit var db: Database\n\n    beforeSpec {\n        db = Database.connect(\"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\")\n        transaction(db) {\n            SchemaUtils.create(UsersTable)\n        }\n    }\n\n    afterSpec {\n        transaction(db) {\n            SchemaUtils.drop(UsersTable)\n        }\n    }\n\n    beforeTest {\n        transaction(db) {\n            UsersTable.deleteAll()\n        }\n    }\n\n    test(\"insert and retrieve user\") {\n        transaction(db) {\n            UsersTable.insert {\n                it[name] = \"Alice\"\n                it[email] = \"alice@example.com\"\n            }\n        }\n\n        val users = transaction(db) {\n            UsersTable.selectAll().map { it[UsersTable.name] }\n        }\n\n        users shouldContain \"Alice\"\n    }\n})\n```\n\n#### Kotest Extensions\n\n```kotlin\n\u002F\u002F Reusable test extension\nclass DatabaseExtension : BeforeSpecListener, AfterSpecListener {\n    lateinit var db: Database\n\n    override suspend fun beforeSpec(spec: Spec) {\n        db = Database.connect(\"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\")\n    }\n\n    override suspend fun afterSpec(spec: Spec) {\n        \u002F\u002F cleanup\n    }\n}\n\nclass UserRepositoryTest : FunSpec({\n    val dbExt = DatabaseExtension()\n    register(dbExt)\n\n    test(\"save and find user\") {\n        val repo = UserRepository(dbExt.db)\n        \u002F\u002F ...\n    }\n})\n```\n\n### Kover Coverage\n\n#### Gradle Configuration\n\n```kotlin\n\u002F\u002F build.gradle.kts\nplugins {\n    id(\"org.jetbrains.kotlinx.kover\") version \"0.9.7\"\n}\n\nkover {\n    reports {\n        total {\n            html { onCheck = true }\n            xml { onCheck = true }\n        }\n        filters {\n            excludes {\n                classes(\"*.generated.*\", \"*.config.*\")\n            }\n        }\n        verify {\n            rule {\n                minBound(80) \u002F\u002F Fail build below 80% coverage\n            }\n        }\n    }\n}\n```\n\n#### Coverage Commands\n\n```bash\n# Run tests with coverage\n.\u002Fgradlew koverHtmlReport\n\n# Verify coverage thresholds\n.\u002Fgradlew koverVerify\n\n# XML report for CI\n.\u002Fgradlew koverXmlReport\n\n# View HTML report (use the command for your OS)\n# macOS:   open build\u002Freports\u002Fkover\u002Fhtml\u002Findex.html\n# Linux:   xdg-open build\u002Freports\u002Fkover\u002Fhtml\u002Findex.html\n# Windows: start build\u002Freports\u002Fkover\u002Fhtml\u002Findex.html\n```\n\n#### Coverage Targets\n\n| Code Type | Target |\n|-----------|--------|\n| Critical business logic | 100% |\n| Public APIs | 90%+ |\n| General code | 80%+ |\n| Generated \u002F config code | Exclude |\n\n### Ktor testApplication Testing\n\n```kotlin\nclass ApiRoutesTest : FunSpec({\n    test(\"GET \u002Fusers returns list\") {\n        testApplication {\n            application {\n                configureRouting()\n                configureSerialization()\n            }\n\n            val response = client.get(\"\u002Fusers\")\n\n            response.status shouldBe HttpStatusCode.OK\n            val users = response.body\u003CList\u003CUserResponse>>()\n            users.shouldNotBeEmpty()\n        }\n    }\n\n    test(\"POST \u002Fusers creates user\") {\n        testApplication {\n            application {\n                configureRouting()\n                configureSerialization()\n            }\n\n            val response = client.post(\"\u002Fusers\") {\n                contentType(ContentType.Application.Json)\n                setBody(CreateUserRequest(\"Alice\", \"alice@example.com\"))\n            }\n\n            response.status shouldBe HttpStatusCode.Created\n        }\n    }\n})\n```\n\n### Testing Commands\n\n```bash\n# Run all tests\n.\u002Fgradlew test\n\n# Run specific test class\n.\u002Fgradlew test --tests \"com.example.UserServiceTest\"\n\n# Run specific test\n.\u002Fgradlew test --tests \"com.example.UserServiceTest.getUser returns user when found\"\n\n# Run with verbose output\n.\u002Fgradlew test --info\n\n# Run with coverage\n.\u002Fgradlew koverHtmlReport\n\n# Run detekt (static analysis)\n.\u002Fgradlew detekt\n\n# Run ktlint (formatting check)\n.\u002Fgradlew ktlintCheck\n\n# Continuous testing\n.\u002Fgradlew test --continuous\n```\n\n### Best Practices\n\n**DO:**\n- Write tests FIRST (TDD)\n- Use Kotest's spec styles consistently across the project\n- Use MockK's `coEvery`\u002F`coVerify` for suspend functions\n- Use `runTest` for coroutine testing\n- Test behavior, not implementation\n- Use property-based testing for pure functions\n- Use `data class` test fixtures for clarity\n\n**DON'T:**\n- Mix testing frameworks (pick Kotest and stick with it)\n- Mock data classes (use real instances)\n- Use `Thread.sleep()` in coroutine tests (use `advanceTimeBy`)\n- Skip the RED phase in TDD\n- Test private functions directly\n- Ignore flaky tests\n\n### Integration with CI\u002FCD\n\n```yaml\n# GitHub Actions example\ntest:\n  runs-on: ubuntu-latest\n  steps:\n    - uses: actions\u002Fcheckout@v4\n    - uses: actions\u002Fsetup-java@v4\n      with:\n        distribution: 'temurin'\n        java-version: '21'\n\n    - name: Run tests with coverage\n      run: .\u002Fgradlew test koverXmlReport\n\n    - name: Verify coverage\n      run: .\u002Fgradlew koverVerify\n\n    - name: Upload coverage\n      uses: codecov\u002Fcodecov-action@v5\n      with:\n        files: build\u002Freports\u002Fkover\u002Freport.xml\n        token: ${{ secrets.CODECOV_TOKEN }}\n```\n\n**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.\n",{"name":5,"origin":39,"description":7},"ECC","main","b0f0a87593c578a7898eccaf1b363f7f87c1f99b","plugins\u002FColin4k1024\u002Ftsp\u002Fskills\u002Fkotlin-testing",490,"https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins","https:\u002F\u002Fhol.org\u002Fplugins\u002Fbest-codex-plugins","https:\u002F\u002Fgithub.com\u002Fhashgraph-online","https:\u002F\u002Fhol.org","Open-source standards and developer tools for interoperable AI agents: discovery, identity, communication, payments, privacy and security.","2026-11-14 02:12:02",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add hashgraph-online\u002Fawesome-codex-plugins --skill kotlin-testing",{"tool":56,"command":57},"bunx","bunx skills add hashgraph-online\u002Fawesome-codex-plugins --skill kotlin-testing",{"tool":59,"command":60},"pnpm","pnpm dlx skills add hashgraph-online\u002Fawesome-codex-plugins --skill kotlin-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002FColin4k1024\u002Ftsp\u002Fskills\u002Fkotlin-testing\u002FSKILL.md",true,[],[65,73,80,87,114,138],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":71,"publishedAt":72},"01M746ZWACVKMZ1BJXJZHYJSSX","migrate-to-e2e","Moves an existing Playwright, Cypress, Selenium, Appium, Detox, Maestro, or TesterArmy suite to e2e one slice at a time, with the old suite running beside the new one until each ported test is proven. Keeps a MIGRATION.md progress file so any later session resumes where the last stopped. Optionally plants a defect to show both suites catch it. Use for \u002Fmigrate-to-e2e, \"migrate our tests to e2e\", \"port these Maestro flows\", \"move our TesterArmy tests\", or when a project has an e2e config and an older suite still running.","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002Ftester-army\u002Fe2e\u002Fskills\u002Fmigrate-to-e2e\u002FSKILL.md",[],[],"2026-11-11 15:11:14",{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":72},"01M746ZPTAX1Q0FBKAB1CYND7E","e2e","Writes, runs, and debugs end-to-end tests for web and mobile apps with e2e, the agentic test runner, which mixes agent.act goals with exact locator assertions on a Playwright browser or an iOS or Android device. Also drives a project's running app live over the e2e MCP server to look at a screen, check a UI change, take a screenshot or record a video, and try a locator; and runs bug bashes, parallel e2e explore runs with each bug proven by a repro test. Use when a project depends on e2e or has an e2e.config.ts, when asked for end-to-end, browser, mobile, or agentic UI tests, to check a change in the running app, to QA or bug bash an app or branch, or when an e2e run fails.","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002Ftester-army\u002Fe2e\u002Fskills\u002Fe2e\u002FSKILL.md",[],[],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":72},"01M746ZJN784HVHMDP9MQGCSGG","create-verification-skill","Generate a project-local verify-\u003Capp> skill on top of e2e, so any coding agent can launch the app, check it is worth driving, exercise a feature the way a user does, keep evidence, and bug bash it. Writes the skill, a feature map with one explore charter per feature, and proves it once. Use for \u002Fcreate-verification-skill, \"make a verification skill for this repo\", or when a project has no scripted way to prove UI behavior.","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002Ftester-army\u002Fe2e\u002Fskills\u002Fcreate-verification-skill\u002FSKILL.md",[],[],{"skillNo":88,"name":89,"nameZh":6,"description":90,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":91,"categories":92,"keywords":93,"publishedAt":34},"01M716A6ETNB9B1SQ3GXZC75VR","deja-search","deja-vu memory — search the user's past AI coding sessions with the deja CLI. Holds what they already settled and never wrote into the repo (naming rules, banned or already-tried tools, required checks before merge, preferred values, who owns what). Use before your first edit in a task, before you call a change done or ready to merge, before you recommend a tool, library, design or reviewer, before re-debugging an error that may already be solved, when they say 'didn't we fix this before', 'what did we decide about X' or 'I already have that', and before telling them something on this machine does not exist.","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002Fvshulcz\u002Fdeja-vu\u002Fskills\u002Fdeja-search\u002FSKILL.md",[],[94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113],"search","deja search","never","error","ready","memory","re","decide","fix","design","debugging","when","you","done","reviewer","io","change","cli","merge","past",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":34},"01M716A3J2W5SENSE9VGMXB375","deja-reflect","End-of-session reflection and memory curation with deja-vu. Review what was decided, what commands broke and what repaired them, and record durable project rules or decisions with deja remember so the next session starts with them.","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002Fvshulcz\u002Fdeja-vu\u002Fskills\u002Fdeja-reflect\u002FSKILL.md",[],[121,122,123,109,32,124,125,126,127,128,129,130,131,132,99,133,134,135,136,137],"deja reflect","reflect","review","record","github","next","https","node","Rules","decisions","brew","remember","project rules","project","session","end","reflection",{"skillNo":139,"name":140,"nameZh":6,"description":141,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":142,"categories":143,"keywords":153,"publishedAt":34},"01M715T3RR4BAYSR57HPPTJ0PF","revid","Make, edit, export and post AI videos with Revid (revid.ai) through the Revid MCP server. Use whenever the user wants a short-form or long-form video made (from an idea, a script, a URL, an article, a PDF, a tweet, a product page, their own clips, or a song), wants to find what is going viral on TikTok, YouTube, Instagram or X before making something, wants to edit or re-export an existing Revid project, generate a standalone AI image or clip, make a series episode, or publish and schedule a finished video to connected social accounts. Covers setup (hosted MCP with OAuth, or an API key), the 36 tools, credits, polling and the rules for publishing. Routes to the more specific revid-* skills when one fits.","https:\u002F\u002Fgithub.com\u002Fhashgraph-online\u002Fawesome-codex-plugins\u002Fblob\u002Fmain\u002Fplugins\u002Ftmakersas\u002Frevid-skills\u002Fskills\u002Frevid\u002FSKILL.md",[144,147,150],{"categoryNo":145,"categoryName":146,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":148,"categoryName":149,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":151,"categoryName":152,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",[140,154,155,156,157,158,159,134,160,100,161,162,163,164,105,165,166,167,168,169],"URL","Video","short-form","standalone","PDF","viral","tiktok","short","social","MCP","song","image","mcp server","schedule","api","publish"]