Source profileQuality 88/100

affaan-m/ECC/docs/zh-CN/skills/kotlin-testing/SKILL.md

kotlin-testing

Use it for testing and engineering tasks; the detail page covers purpose, installation, and practical steps.

Source repository stars
234,327
Declared platforms
0
Static risk flags
0
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/kotlin-testing"
    Safe inspection promptEditorial

    Inspect the Agent Skill "kotlin-testing" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/zh-CN/skills/kotlin-testing/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      何时使用

      编写新的 Kotlin 函数或类 为现有 Kotlin 代码添加测试覆盖率 实现基于属性的测试 在 Kotlin 项目中遵循 TDD 工作流 为代码覆盖率配置 Kover

      编写新的 Kotlin 函数或类为现有 Kotlin 代码添加测试覆盖率实现基于属性的测试
    2. 02

      工作原理

      1. 确定目标代码 — 找到要测试的函数、类或模块 2. 编写 Kotest 规范 — 选择与测试范围匹配的规范样式(StringSpec、FunSpec、BehaviorSpec) 3. 模拟依赖项 — 使用 MockK 来隔离被测单元 4. 运行测试(红色阶段) — 验证测试是否按预期失败 5. 实现代码(绿色阶段) — 编写最少的代码以使测试通过 6. 重构 — 改进实现,同时保持测试通过 7. 检查覆盖率 — 运行 ./gradlew koverHtmlReport 并验证 80%+ 的覆盖率

      确定目标代码 — 找到要测试的函数、类或模块编写 Kotest 规范 — 选择与测试范围匹配的规范样式(StringSpec、FunSpec、BehaviorSpec)模拟依赖项 — 使用 MockK 来隔离被测单元
    3. 03

      示例

      Kotest 规范 — Kotest 规范样式 中的 StringSpec、FunSpec、BehaviorSpec、DescribeSpec 示例 模拟 — MockK 中的 MockK 设置、协程模拟、参数捕获 TDD 演练 — Kotlin 的 TDD 工作流 中 EmailValidator 的完整 RED/GREEN/REFACTOR 周期 覆盖率 — Kover 覆盖率 中的 Kover 配置和命令 Ktor 测试 — Ktor testApplication 测试 中的 testApplication 设置

      Kotest 规范 — Kotest 规范样式 中的 StringSpec、FunSpec、BehaviorSpec、DescribeSpec 示例模拟 — MockK 中的 MockK 设置、协程模拟、参数捕获TDD 演练 — Kotlin 的 TDD 工作流 中 EmailValidator 的完整 RED/GREEN/REFACTOR 周期
    4. 04

      快速参考

      Kotest 规范 — Kotest 规范样式 中的 StringSpec、FunSpec、BehaviorSpec、DescribeSpec 示例 模拟 — MockK 中的 MockK 设置、协程模拟、参数捕获 TDD 演练 — Kotlin 的 TDD 工作流 中 EmailValidator 的完整 RED/GREEN/REFACTOR 周期 覆盖率 — Kover 覆盖率 中的 Kover 配置和命令 Ktor 测试 — Ktor testApplication 测试 中的 testApplication 设置

      Kotest 规范 — Kotest 规范样式 中的 StringSpec、FunSpec、BehaviorSpec、DescribeSpec 示例模拟 — MockK 中的 MockK 设置、协程模拟、参数捕获TDD 演练 — Kotlin 的 TDD 工作流 中 EmailValidator 的完整 RED/GREEN/REFACTOR 周期
    5. 05

      Kotlin 的 TDD 工作流

      Review the “Kotlin 的 TDD 工作流” section in the pinned source before continuing.

      Review and apply the “Kotlin 的 TDD 工作流” source section.

    Permission review

    Static risk signals and limitations

    No configured static risk pattern was detected

    This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score88/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    docs/zh-CN/skills/kotlin-testing/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    Kotlin 测试模式

    遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。

    何时使用

    • 编写新的 Kotlin 函数或类
    • 为现有 Kotlin 代码添加测试覆盖率
    • 实现基于属性的测试
    • 在 Kotlin 项目中遵循 TDD 工作流
    • 为代码覆盖率配置 Kover

    工作原理

    1. 确定目标代码 — 找到要测试的函数、类或模块
    2. 编写 Kotest 规范 — 选择与测试范围匹配的规范样式(StringSpec、FunSpec、BehaviorSpec)
    3. 模拟依赖项 — 使用 MockK 来隔离被测单元
    4. 运行测试(红色阶段) — 验证测试是否按预期失败
    5. 实现代码(绿色阶段) — 编写最少的代码以使测试通过
    6. 重构 — 改进实现,同时保持测试通过
    7. 检查覆盖率 — 运行 ./gradlew koverHtmlReport 并验证 80%+ 的覆盖率

    示例

    以下部分包含每个测试模式的详细、可运行示例:

    快速参考

    Kotlin 的 TDD 工作流

    RED-GREEN-REFACTOR 周期

    RED     -> 首先编写一个失败的测试
    GREEN   -> 编写最少的代码使测试通过
    REFACTOR -> 改进代码同时保持测试通过
    REPEAT  -> 继续下一个需求
    

    Kotlin 中逐步进行 TDD

    // Step 1: Define the interface/signature
    // EmailValidator.kt
    package com.example.validator
    
    fun validateEmail(email: String): Result<String> {
        TODO("not implemented")
    }
    
    // Step 2: Write failing test (RED)
    // EmailValidatorTest.kt
    package com.example.validator
    
    import io.kotest.core.spec.style.StringSpec
    import io.kotest.matchers.result.shouldBeFailure
    import io.kotest.matchers.result.shouldBeSuccess
    
    class EmailValidatorTest : StringSpec({
        "valid email returns success" {
            validateEmail("user@example.com").shouldBeSuccess("user@example.com")
        }
    
        "empty email returns failure" {
            validateEmail("").shouldBeFailure()
        }
    
        "email without @ returns failure" {
            validateEmail("userexample.com").shouldBeFailure()
        }
    })
    
    // Step 3: Run tests - verify FAIL
    // $ ./gradlew test
    // EmailValidatorTest > valid email returns success FAILED
    //   kotlin.NotImplementedError: An operation is not implemented
    
    // Step 4: Implement minimal code (GREEN)
    fun validateEmail(email: String): Result<String> {
        if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
        if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
        val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
        if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
        return Result.success(email)
    }
    
    // Step 5: Run tests - verify PASS
    // $ ./gradlew test
    // EmailValidatorTest > valid email returns success PASSED
    // EmailValidatorTest > empty email returns failure PASSED
    // EmailValidatorTest > email without @ returns failure PASSED
    
    // Step 6: Refactor if needed, verify tests still pass
    

    Kotest 规范样式

    StringSpec(最简单)

    class CalculatorTest : StringSpec({
        "add two positive numbers" {
            Calculator.add(2, 3) shouldBe 5
        }
    
        "add negative numbers" {
            Calculator.add(-1, -2) shouldBe -3
        }
    
        "add zero" {
            Calculator.add(0, 5) shouldBe 5
        }
    })
    

    FunSpec(类似 JUnit)

    class UserServiceTest : FunSpec({
        val repository = mockk<UserRepository>()
        val service = UserService(repository)
    
        test("getUser returns user when found") {
            val expected = User(id = "1", name = "Alice")
            coEvery { repository.findById("1") } returns expected
    
            val result = service.getUser("1")
    
            result shouldBe expected
        }
    
        test("getUser throws when not found") {
            coEvery { repository.findById("999") } returns null
    
            shouldThrow<UserNotFoundException> {
                service.getUser("999")
            }
        }
    })
    

    BehaviorSpec(BDD 风格)

    class OrderServiceTest : BehaviorSpec({
        val repository = mockk<OrderRepository>()
        val paymentService = mockk<PaymentService>()
        val service = OrderService(repository, paymentService)
    
        Given("a valid order request") {
            val request = CreateOrderRequest(
                userId = "user-1",
                items = listOf(OrderItem("product-1", quantity = 2)),
            )
    
            When("the order is placed") {
                coEvery { paymentService.charge(any()) } returns PaymentResult.Success
                coEvery { repository.save(any()) } answers { firstArg() }
    
                val result = service.placeOrder(request)
    
                Then("it should return a confirmed order") {
                    result.status shouldBe OrderStatus.CONFIRMED
                }
    
                Then("it should charge payment") {
                    coVerify(exactly = 1) { paymentService.charge(any()) }
                }
            }
    
            When("payment fails") {
                coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
    
                Then("it should throw PaymentException") {
                    shouldThrow<PaymentException> {
                        service.placeOrder(request)
                    }
                }
            }
        }
    })
    

    DescribeSpec(RSpec 风格)

    class UserValidatorTest : DescribeSpec({
        describe("validateUser") {
            val validator = UserValidator()
    
            context("with valid input") {
                it("accepts a normal user") {
                    val user = CreateUserRequest("Alice", "alice@example.com")
                    validator.validate(user).shouldBeValid()
                }
            }
    
            context("with invalid name") {
                it("rejects blank name") {
                    val user = CreateUserRequest("", "alice@example.com")
                    validator.validate(user).shouldBeInvalid()
                }
    
                it("rejects name exceeding max length") {
                    val user = CreateUserRequest("A".repeat(256), "alice@example.com")
                    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.*
    
    // Equality
    result shouldBe expected
    result shouldNotBe unexpected
    
    // Strings
    name shouldStartWith "Al"
    name shouldEndWith "ice"
    name shouldContain "lic"
    name shouldMatch Regex("[A-Z][a-z]+")
    name.shouldBeBlank()
    
    // Collections
    list shouldContain "item"
    list shouldHaveSize 3
    list.shouldBeSorted()
    list.shouldContainAll("a", "b", "c")
    list.shouldBeEmpty()
    
    // Nulls
    result.shouldNotBeNull()
    result.shouldBeNull()
    
    // Types
    result.shouldBeInstanceOf<User>()
    
    // Numbers
    count shouldBeGreaterThan 0
    price shouldBeInRange 1.0..100.0
    
    // Exceptions
    shouldThrow<IllegalArgumentException> {
        validateAge(-1)
    }.message shouldBe "Age must be positive"
    
    shouldNotThrow<Exception> {
        validateAge(25)
    }
    

    自定义匹配器

    fun beActiveUser() = object : Matcher<User> {
        override fun test(value: User) = MatcherResult(
            value.isActive && value.lastLogin != null,
            { "User ${value.id} should be active with a last login" },
            { "User ${value.id} should not be active" },
        )
    }
    
    // Usage
    user should beActiveUser()
    

    MockK

    基本模拟

    class UserServiceTest : FunSpec({
        val repository = mockk<UserRepository>()
        val logger = mockk<Logger>(relaxed = true) // Relaxed: returns defaults
        val service = UserService(repository, logger)
    
        beforeTest {
            clearMocks(repository, logger)
        }
    
        test("findUser delegates to repository") {
            val expected = User(id = "1", name = "Alice")
            every { repository.findById("1") } returns expected
    
            val result = service.findUser("1")
    
            result shouldBe expected
            verify(exactly = 1) { repository.findById("1") }
        }
    
        test("findUser returns null for unknown id") {
            every { repository.findById(any()) } returns null
    
            val result = service.findUser("unknown")
    
            result.shouldBeNull()
        }
    })
    

    协程模拟

    class AsyncUserServiceTest : FunSpec({
        val repository = mockk<UserRepository>()
        val service = UserService(repository)
    
        test("getUser suspending function") {
            coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
    
            val result = service.getUser("1")
    
            result.name shouldBe "Alice"
            coVerify { repository.findById("1") }
        }
    
        test("getUser with delay") {
            coEvery { repository.findById("1") } coAnswers {
                delay(100) // Simulate async work
                User(id = "1", name = "Alice")
            }
    
            val result = service.getUser("1")
            result.name shouldBe "Alice"
        }
    })
    

    参数捕获

    test("save captures the user argument") {
        val slot = slot<User>()
        coEvery { repository.save(capture(slot)) } returns Unit
    
        service.createUser(CreateUserRequest("Alice", "alice@example.com"))
    
        slot.captured.name shouldBe "Alice"
        slot.captured.email shouldBe "alice@example.com"
        slot.captured.id.shouldNotBeNull()
    }
    

    间谍和部分模拟

    test("spy on real object") {
        val realService = UserService(repository)
        val spy = spyk(realService)
    
        every { spy.generateId() } returns "fixed-id"
    
        spy.createUser(request)
    
        verify { spy.generateId() } // Overridden
        // Other methods use real implementation
    }
    

    协程测试

    用于挂起函数的 runTest

    import kotlinx.coroutines.test.runTest
    
    class CoroutineServiceTest : FunSpec({
        test("concurrent fetches complete together") {
            runTest {
                val service = DataService(testScope = this)
    
                val result = service.fetchAllData()
    
                result.users.shouldNotBeEmpty()
                result.products.shouldNotBeEmpty()
            }
        }
    
        test("timeout after delay") {
            runTest {
                val service = SlowService()
    
                shouldThrow<TimeoutCancellationException> {
                    withTimeout(100) {
                        service.slowOperation() // Takes > 100ms
                    }
                }
            }
        }
    })
    

    测试 Flow

    import io.kotest.matchers.collections.shouldContainInOrder
    import kotlinx.coroutines.flow.MutableSharedFlow
    import kotlinx.coroutines.flow.toList
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.test.advanceTimeBy
    import kotlinx.coroutines.test.runTest
    
    class FlowServiceTest : FunSpec({
        test("observeUsers emits updates") {
            runTest {
                val service = UserFlowService()
    
                val emissions = service.observeUsers()
                    .take(3)
                    .toList()
    
                emissions shouldHaveSize 3
                emissions.last().shouldNotBeEmpty()
            }
        }
    
        test("searchUsers debounces input") {
            runTest {
                val service = SearchService()
                val queries = MutableSharedFlow<String>()
    
                val results = mutableListOf<List<User>>()
                val job = launch {
                    service.searchUsers(queries).collect { results.add(it) }
                }
    
                queries.emit("a")
                queries.emit("ab")
                queries.emit("abc") // Only this should trigger search
                advanceTimeBy(500)
    
                results shouldHaveSize 1
                job.cancel()
            }
        }
    })
    

    TestDispatcher

    import kotlinx.coroutines.test.StandardTestDispatcher
    import kotlinx.coroutines.test.advanceUntilIdle
    
    class DispatcherTest : FunSpec({
        test("uses test dispatcher for controlled execution") {
            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
    
    // Note: The serialization roundtrip test below requires the User data class
    // to be annotated with @Serializable (from kotlinx.serialization).
    
    class PropertyTest : FunSpec({
        test("string reverse is involutory") {
            forAll<String> { s ->
                s.reversed().reversed() == s
            }
        }
    
        test("list sort is idempotent") {
            forAll(Arb.list(Arb.int())) { list ->
                list.sorted() == list.sorted().sorted()
            }
        }
    
        test("serialization roundtrip preserves data") {
            checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
                User(name = name, email = "$email@test.com")
            }) { user ->
                val json = Json.encodeToString(user)
                val decoded = Json.decodeFromString<User>(json)
                decoded shouldBe user
            }
        }
    })
    

    自定义生成器

    val userArb: Arb<User> = Arb.bind(
        Arb.string(minSize = 1, maxSize = 50),
        Arb.email(),
        Arb.enum<Role>(),
    ) { name, email, role ->
        User(
            id = UserId(UUID.randomUUID().toString()),
            name = name,
            email = Email(email),
            role = role,
        )
    }
    
    val moneyArb: Arb<Money> = Arb.bind(
        Arb.long(1L..1_000_000L),
        Arb.enum<Currency>(),
    ) { amount, currency ->
        Money(amount, currency)
    }
    

    数据驱动测试

    Kotest 中的 withData

    class ParserTest : FunSpec({
        context("parsing valid dates") {
            withData(
                "2026-01-15" to LocalDate(2026, 1, 15),
                "2026-12-31" to LocalDate(2026, 12, 31),
                "2000-01-01" to LocalDate(2000, 1, 1),
            ) { (input, expected) ->
                parseDate(input) shouldBe expected
            }
        }
    
        context("rejecting invalid dates") {
            withData(
                nameFn = { "rejects '$it'" },
                "not-a-date",
                "2026-13-01",
                "2026-00-15",
                "",
            ) { input ->
                shouldThrow<DateParseException> {
                    parseDate(input)
                }
            }
        }
    })
    

    测试生命周期和固件

    BeforeTest / AfterTest

    class DatabaseTest : FunSpec({
        lateinit var db: Database
    
        beforeSpec {
            db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
            transaction(db) {
                SchemaUtils.create(UsersTable)
            }
        }
    
        afterSpec {
            transaction(db) {
                SchemaUtils.drop(UsersTable)
            }
        }
    
        beforeTest {
            transaction(db) {
                UsersTable.deleteAll()
            }
        }
    
        test("insert and retrieve user") {
            transaction(db) {
                UsersTable.insert {
                    it[name] = "Alice"
                    it[email] = "alice@example.com"
                }
            }
    
            val users = transaction(db) {
                UsersTable.selectAll().map { it[UsersTable.name] }
            }
    
            users shouldContain "Alice"
        }
    })
    

    Kotest 扩展

    // Reusable test extension
    class DatabaseExtension : BeforeSpecListener, AfterSpecListener {
        lateinit var db: Database
    
        override suspend fun beforeSpec(spec: Spec) {
            db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
        }
    
        override suspend fun afterSpec(spec: Spec) {
            // cleanup
        }
    }
    
    class UserRepositoryTest : FunSpec({
        val dbExt = DatabaseExtension()
        register(dbExt)
    
        test("save and find user") {
            val repo = UserRepository(dbExt.db)
            // ...
        }
    })
    

    Kover 覆盖率

    Gradle 配置

    // build.gradle.kts
    plugins {
        id("org.jetbrains.kotlinx.kover") version "0.9.7"
    }
    
    kover {
        reports {
            total {
                html { onCheck = true }
                xml { onCheck = true }
            }
            filters {
                excludes {
                    classes("*.generated.*", "*.config.*")
                }
            }
            verify {
                rule {
                    minBound(80) // Fail build below 80% coverage
                }
            }
        }
    }
    

    覆盖率命令

    # Run tests with coverage
    ./gradlew koverHtmlReport
    
    # Verify coverage thresholds
    ./gradlew koverVerify
    
    # XML report for CI
    ./gradlew koverXmlReport
    
    # View HTML report (use the command for your OS)
    # macOS:   open build/reports/kover/html/index.html
    # Linux:   xdg-open build/reports/kover/html/index.html
    # Windows: start build/reports/kover/html/index.html
    

    覆盖率目标

    代码类型目标
    关键业务逻辑100%
    公共 API90%+
    通用代码80%+
    生成的 / 配置代码排除

    Ktor testApplication 测试

    class ApiRoutesTest : FunSpec({
        test("GET /users returns list") {
            testApplication {
                application {
                    configureRouting()
                    configureSerialization()
                }
    
                val response = client.get("/users")
    
                response.status shouldBe HttpStatusCode.OK
                val users = response.body<List<UserResponse>>()
                users.shouldNotBeEmpty()
            }
        }
    
        test("POST /users creates user") {
            testApplication {
                application {
                    configureRouting()
                    configureSerialization()
                }
    
                val response = client.post("/users") {
                    contentType(ContentType.Application.Json)
                    setBody(CreateUserRequest("Alice", "alice@example.com"))
                }
    
                response.status shouldBe HttpStatusCode.Created
            }
        }
    })
    

    测试命令

    # Run all tests
    ./gradlew test
    
    # Run specific test class
    ./gradlew test --tests "com.example.UserServiceTest"
    
    # Run specific test
    ./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
    
    # Run with verbose output
    ./gradlew test --info
    
    # Run with coverage
    ./gradlew koverHtmlReport
    
    # Run detekt (static analysis)
    ./gradlew detekt
    
    # Run ktlint (formatting check)
    ./gradlew ktlintCheck
    
    # Continuous testing
    ./gradlew test --continuous
    

    最佳实践

    应做:

    • 先写测试(TDD)
    • 在整个项目中一致地使用 Kotest 的规范样式
    • 对挂起函数使用 MockK 的 coEvery/coVerify
    • 对协程测试使用 runTest
    • 测试行为,而非实现
    • 对纯函数使用基于属性的测试
    • 为清晰起见使用 data class 测试固件

    不应做:

    • 混合使用测试框架(选择 Kotest 并坚持使用)
    • 模拟数据类(使用真实实例)
    • 在协程测试中使用 Thread.sleep()(改用 advanceTimeBy
    • 跳过 TDD 中的红色阶段
    • 直接测试私有函数
    • 忽略不稳定的测试

    与 CI/CD 集成

    # GitHub Actions example
    test:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-java@v4
          with:
            distribution: 'temurin'
            java-version: '21'
    
        - name: Run tests with coverage
          run: ./gradlew test koverXmlReport
    
        - name: Verify coverage
          run: ./gradlew koverVerify
    
        - name: Upload coverage
          uses: codecov/codecov-action@v5
          with:
            files: build/reports/kover/report.xml
            token: ${{ secrets.CODECOV_TOKEN }}
    

    记住:测试就是文档。它们展示了你的 Kotlin 代码应如何使用。使用 Kotest 富有表现力的匹配器使测试可读,并使用 MockK 来清晰地模拟依赖项。

    Alternatives

    Compare before choosing