Skills Plugins MCP Prompt Model 博客 我的中心

kotlin-testing

使用Kotest、MockK、协程测试、基于属性的测试和Kover覆盖率的Kotlin测试模式。遵循TDD方法论和地道的Kotlin实践。

DeepseekModel Curated skill Quality Excellent · 90 v1.0.0

Get

https://deepseekmodel.com/api/download.php?id=affaan-m-ecc-docs-zh-cn-skills-kotlin-testing-skill-md&format=skill
Download .skill Standard format with system_prompt and model_config, ready for any agent framework
The actual content of the system_prompt field in the .skill file.
name kotlin-testing description 使用Kotest、MockK、协程测试、基于属性的测试和Kover覆盖率的Kotlin测试模式。遵循TDD方法论和地道的Kotlin实践。 origin ECC Kotlin 测试模式 遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。 何时使用 编写新的 Kotlin 函数或类 为现有 Kotlin 代码添加测试覆盖率 实现基于属性的测试 在 Kotlin 项目中遵循 TDD 工作流 为代码覆盖率配置 Kover 工作原理 确定目标代码 — 找到要测试的函数、类或模块 编写 Kotest 规范 — 选择与测试范围匹配的规范样式(StringSpec、FunSpec、BehaviorSpec) 模拟依赖项 — 使用 MockK 来隔离被测单元 运行测试(红色阶段) — 验证测试是否按预期失败 实现代码(绿色阶段) — 编写最少的代码以使测试通过 重构 — 改进实现,同时保持测试通过 检查覆盖率 — 运行 ./gradlew koverHtmlReport 并验证 80%+ 的覆盖率 示例 以下部分包含每个测试模式的详细、可运行示例: 快速参考 Kotest 规范 — Kotest 规范样式 中的 StringSpec、FunSpec、BehaviorSpec、DescribeSpec 示例 模拟 — MockK 中的 MockK 设置、协程模拟、参数捕获 TDD 演练 — Kotlin 的 TDD 工作流 中 EmailValidator 的完整 RED/GREEN/REFACTOR 周期 覆盖率 — Kover 覆盖率 中的 Kover 配置和命令 Ktor 测试 — Ktor testApplication 测试 中的 testApplication 设置 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 } }
Keywords that activate this skill. Click one to copy it.

This skill does not provide trigger words.

The downloaded .skill package contains the following fields.
Field Description
formatFormat tag (skill/v1)
skill_idUnique skill ID
nameSkill name
versionVersion
descriptionDescription
categoryCategories (array)
trigger_wordsTrigger words
tagsTags
sourceSource
source_urlSource URL (this page)
exported_atExported at (set per download)
system_promptSystem prompt body
model_configModel config: provider / model / temperature / max_tokens / top_p
examplesExamples
install_guideImport guide for Coze / Dify / Claude / custom frameworks
The same skill can be exported in different platform formats.
.skill Standard format with system_prompt and model_config, ready for any agent framework Download
.skillpro Enhanced format with scripts, tools, dependencies and hooks Download
.json Plain JSON export with system_prompt and model parameters only Download
Coze Markdown with frontmatter, for Coze platform import Download
Dify Dify DSL, import directly after creating an app Download

每日精选 Skill 推荐,免费送到你邮箱

输入邮箱,每天接收一个精选 AI Agent 技能推荐。完全免费,持续更新。

提交后我们会发送一封确认邮件,点击邮件里的链接才会开始收信。

完全免费,取消任意时间。我们不会发送垃圾邮件。