Best fit
- 使用Kotest、MockK、协程测试、基于属性的测试和Kover覆盖率的Kotlin测试模式。遵循TDD方法论和地道的Kotlin实践。
affaan-m/ECC
使用Kotest、MockK、协程测试、基于属性的测试和Kover覆盖率的Kotlin测试模式。遵循TDD方法论和地道的Kotlin实践。
npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/kotlin-testing"Source checked Jul 28, 2026·Refresh due Oct 26, 2026
Reorganized from the pinned upstream SKILL.md
According to the pinned SKILL.md from affaan-m/ECC: 遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。
npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/kotlin-testing"Best fit
Bring this context
Expected outputs
Key source sections
Sections are extracted automatically from the pinned SKILL.md and link back to the source.
编写新的 Kotlin 函数或类 为现有 Kotlin 代码添加测试覆盖率 实现基于属性的测试 在 Kotlin 项目中遵循 TDD 工作流 为代码覆盖率配置 Kover
1. 确定目标代码 — 找到要测试的函数、类或模块 2. 编写 Kotest 规范 — 选择与测试范围匹配的规范样式(StringSpec、FunSpec、BehaviorSpec) 3. 模拟依赖项 — 使用 MockK 来隔离被测单元 4. 运行测试(红色阶段) — 验证测试是否按预期失败 5. 实现代码(绿色阶段) — 编写最少的代码以使测试通过 6. 重构 — 改进实现,同时保持测试通过 7. 检查覆盖率 — 运行 ./gradlew koverHtmlReport 并验证 80%+ 的覆盖率
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 周期 覆盖率 — Kover 覆盖率 中的 Kover 配置和命令 Ktor 测试 — Ktor testApplication 测试 中的 testApplication 设置
Review the “Kotlin 的 TDD 工作流” section in the pinned source before continuing.
SkillSignal prompt templates
These prompts were written by SkillSignal from the source structure; they are not upstream text.
Task-start prompt
Confirm source fit, inputs, and outputs before acting.
Use kotlin-testing to help me with: [specific task]. Context: [files, data, or background]. Constraints: [environment, scope, and prohibited actions]. Before acting, check the pinned SKILL.md and explain which sections apply, what inputs are still missing, and what you will deliver.
Source-guided execution
Make the Agent explicitly follow the key extracted sections.
Apply the pinned kotlin-testing source to [task]. Pay particular attention to these source sections: “何时使用”, “工作原理”, “示例”, “快速参考”, “Kotlin 的 TDD 工作流”. Preserve the important decision at each step. Mark facts not covered by the source as “needs confirmation” instead of inventing them. Then verify the result against my acceptance criteria: [criteria].
Result-review prompt
Check omissions, permissions, and source drift before delivery.
Review the current kotlin-testing result: (1) does it satisfy the original task; (2) were any applicable steps or limits in the pinned SKILL.md missed; (3) did it perform any unauthorized file, command, network, or data action; and (4) which conclusions remain unverified? List issues first, then fix only what the source or user authorization supports.
Output checklist
The task matches the purpose documented in the SKILL.md.
The source section “何时使用” has been checked.
The source section “工作原理” has been checked.
The source section “示例” has been checked.
The source section “快速参考” has been checked.
Inputs, constraints, and acceptance criteria are explicit.
Unverified facts, compatibility, and outcome claims are clearly marked.
Any file, command, network, or data action has been reviewed.
Choose a different workflow
Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detailKotest, MockK, coroutine testi, property-based testing ve Kover coverage ile Kotlin test kalıpları. İdiomatic Kotlin uygulamalarıyla TDD metodolojisini takip eder.
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detailPatrones de pruebas Kotlin con Kotest, MockK, pruebas de coroutines, pruebas basadas en propiedades y cobertura con Kover. Sigue la metodología TDD con prácticas idiomáticas de Kotlin.
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detailFAQ
遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。
The catalog detected this source-specific install command: npx skills add https://github.com/affaan-m/ECC --skill "docs/zh-CN/skills/kotlin-testing". Inspect the command and pinned source before running it.
No dedicated Agent platform is declared in the pinned source record.
Quality breakdown
Based on traceable docs and repository signals; stars are not treated as quality.
Compare before choosing
These links are selected from shared tasks, functions, stacks, platforms, and same-name variants. Compare the source owner, documentation, permissions, and maintenance signals.
Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.
Kotest, MockK, coroutine testi, property-based testing ve Kover coverage ile Kotlin test kalıpları. İdiomatic Kotlin uygulamalarıyla TDD metodolojisini takip eder.
Patrones de pruebas Kotlin con Kotest, MockK, pruebas de coroutines, pruebas basadas en propiedades y cobertura con Kover. Sigue la metodología TDD con prácticas idiomáticas de Kotlin.
When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program
Use BEFORE writing/changing tests, adding mocks, or test-only methods on production classes — vs mocking-the-mock, production pollution, partial mocks, and overfit/tautological assertions
遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。
./gradlew koverHtmlReport 并验证 80%+ 的覆盖率以下部分包含每个测试模式的详细、可运行示例:
RED -> 首先编写一个失败的测试
GREEN -> 编写最少的代码使测试通过
REFACTOR -> 改进代码同时保持测试通过
REPEAT -> 继续下一个需求
// 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
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
}
})
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")
}
}
})
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)
}
}
}
}
})
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()
}
}
}
})
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()
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
}
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
}
}
}
}
})
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()
}
}
})
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
}
}
})
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)
}
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)
}
}
}
})
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"
}
})
// 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)
// ...
}
})
// 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% |
| 公共 API | 90%+ |
| 通用代码 | 80%+ |
| 生成的 / 配置代码 | 排除 |
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
应做:
coEvery/coVerifyrunTestdata class 测试固件不应做:
Thread.sleep()(改用 advanceTimeBy)# 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 来清晰地模拟依赖项。