| name | kotlin-testing |
| description | Kotlin testing with JUnit 5, MockK, Spring test slices, and fast feedback commands. Provides single test execution, incremental builds, and JetBrains MCP integration for rapid TDD cycles. Use when writing tests for Kotlin/Spring code, running specific tests, or debugging test failures. |
Kotlin Testing
🚀 MANDATORY: Fast Feedback Workflow
이 워크플로우를 반드시 따르세요. 개발 속도가 10배 이상 빨라집니다.
개발 사이클 (매 코드 수정마다 반복)
1. 코드 수정
2. IDE 검사 (0-2초) → jetbrains.get_file_problems(...)
3. 단일 테스트 (5-10초) → ./gradlew :module:test --tests "*Test"
4. 반복
5. 기능 완료 후 (1회만) → ./gradlew build
⚠️ 금지 패턴 (절대 사용 금지)
./gradlew clean build
./gradlew test
./gradlew clean
./gradlew :module-core-domain:test --tests "*ServiceTest"
./gradlew :module-server-api:test --tests "*ControllerTest"
JetBrains MCP 먼저 사용 (STEP 1)
코드 수정 후 항상 IDE 검사 먼저 실행:
jetbrains.get_file_problems(
filePath="module-core-domain/src/main/kotlin/.../Service.kt",
errorsOnly=True
)
jetbrains.execute_terminal_command(
command="./gradlew :module-core-domain:test --tests '*ServiceTest'",
timeout=60000
)
When to Use
- Writing unit tests for services/repositories
- Running single tests during TDD (fast feedback)
- Setting up integration tests with Testcontainers
- Mocking dependencies with MockK
- Debugging flaky or failing tests
Fast Feedback Commands Reference
Never use ./gradlew clean build during development iterations!
Command Selection Guide
| Situation | Command | Time |
|---|
| Single test class | ./gradlew :module:test --tests "*ServiceTest" | ~5-10s |
| Single test method | ./gradlew :module:test --tests "*ServiceTest.should*" | ~5-10s |
| Module tests only | ./gradlew :module:test | ~15-30s |
| Compile check only | ./gradlew :module:compileKotlin | ~3-5s |
| Full build (CI only) | ./gradlew clean build | ~60-120s |
Module Names Reference
:module-core-common:test
:module-core-domain:test
:module-core-infra:test
:module-server-api:test
Single Test Patterns
./gradlew :module-core-domain:test --tests "*PipelineServiceTest"
./gradlew :module-core-domain:test --tests "*PipelineServiceTest.should create*"
./gradlew :module-core-domain:test --tests "*Service*"
./gradlew :module-core-domain:test --tests "*ServiceTest" --info
./gradlew :module-core-domain:test --tests "*ServiceTest" --rerun
Incremental Build (NO clean!)
./gradlew :module-core-domain:compileKotlin
./gradlew :module-core-domain:compileKotlin && \
./gradlew :module-core-domain:test --tests "*ServiceTest"
./gradlew :module-core-domain:build
./gradlew build
JetBrains MCP Integration (Fastest)
jetbrains.get_file_problems(
filePath="module-core-domain/src/main/kotlin/.../Service.kt",
errorsOnly=True,
timeout=5000
)
jetbrains.execute_terminal_command(
command="./gradlew :module-core-domain:test --tests '*ServiceTest'",
timeout=60000,
maxLinesCount=200
)
jetbrains.get_run_configurations()
jetbrains.execute_run_configuration(
configurationName="ServiceTest",
timeout=30000
)
TDD Fast Feedback Loop
1. Write Test (RED)
@Test
fun `should reject duplicate names`() {
every { repository.existsByName("existing") } returns true
assertThrows<DuplicateNameException> {
service.create(CreateCommand(name = "existing"))
}
}
2. Quick Compile Check
jetbrains.get_file_problems(filePath="...", errorsOnly=True)
./gradlew :module-core-domain:compileKotlin
3. Run Single Test (GREEN)
./gradlew :module-core-domain:test --tests "*ServiceTest"
jetbrains.execute_terminal_command(
command="./gradlew :module-core-domain:test --tests '*ServiceTest'",
timeout=60000
)
4. Refactor
Repeat steps 2-3 until passing, then refactor.
5. Final Module Verification
./gradlew :module-core-domain:test
./gradlew build
MCP Workflow
jetbrains.get_file_problems(
filePath="module-core-domain/src/test/kotlin/.../ServiceTest.kt",
errorsOnly=True
)
serena.search_for_pattern(
"@Test|@MockK|@SpringBootTest",
relative_path="project-basecamp-server/src/test/",
context_lines_after=1,
max_answer_chars=3000
)
jetbrains.execute_terminal_command(
command="./gradlew :module-core-domain:test --tests '*ServiceTest' --info",
timeout=60000
)
context7.get-library-docs("/spring/spring-boot", "testing")
Test Slice Selection
| Slice | Use When | Loads | Time |
|---|
| None (unit test) | Service/domain logic | Nothing | ~1s |
@WebMvcTest | Controller only | Web layer | ~3-5s |
@DataJpaTest | Repository only | JPA + DB | ~5-10s |
@SpringBootTest | Full integration | Everything | ~10-30s |
Rule: Start with unit tests (no slice), add slices only when testing integration points.
MockK Patterns
Basic Mocking
@ExtendWith(MockKExtension::class)
class PipelineServiceTest {
@MockK
private lateinit var repository: PipelineRepositoryJpa
@InjectMockKs
private lateinit var service: PipelineService
@Test
fun `should create pipeline`() {
val command = CreatePipelineCommand(name = "test")
val entity = PipelineEntity(id = 1, name = "test")
every { repository.save(any()) } returns entity
val result = service.create(command)
assertThat(result.name).isEqualTo("test")
verify(exactly = 1) { repository.save(any()) }
}
}
Relaxed Mocks (Stubs)
@MockK(relaxed = true)
private lateinit var logger: Logger
val mockRepo = mockk<Repository>(relaxed = true)
Capturing Arguments
val slot = slot<PipelineEntity>()
every { repository.save(capture(slot)) } returns mockEntity
service.create(command)
assertThat(slot.captured.name).isEqualTo("test")
Coroutines (coEvery/coVerify)
coEvery { asyncService.process(any()) } returns Result.success()
coVerify { asyncService.process(match { it.id == 1L }) }
Spring Test Patterns
@DataJpaTest (Repository Testing)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
class UserRepositoryTest {
companion object {
@Container
val mysql = MySQLContainer("mysql:8.0")
.withDatabaseName("test")
@DynamicPropertySource
@JvmStatic
fun properties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl)
registry.add("spring.datasource.username", mysql::getUsername)
registry.add("spring.datasource.password", mysql::getPassword)
}
}
@Autowired
private lateinit var repository: UserRepositoryJpaSpringData
@Test
fun `should find by email`() {
val user = UserEntity(email = "test@example.com")
repository.save(user)
val found = repository.findByEmail("test@example.com")
assertThat(found?.email).isEqualTo("test@example.com")
}
}
@WebMvcTest (Controller Testing)
@WebMvcTest(PipelineController::class)
class PipelineControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockkBean
private lateinit var pipelineService: PipelineService
@Test
fun `should return pipeline by id`() {
every { pipelineService.findById(1L) } returns PipelineDto(id = 1, name = "test")
mockMvc.get("/api/pipelines/1")
.andExpect {
status { isOk() }
jsonPath("$.name") { value("test") }
}
}
}
Anti-Patterns
| Pattern | Problem | Solution |
|---|
./gradlew clean build in TDD | 60+ seconds per iteration | Single test: --tests "*Test" |
@SpringBootTest for unit tests | Slow, loads everything | Remove annotation |
| Running all tests after each change | Time waste | Run affected test class only |
every { } returns without verify | Mock unused | Add verify or use relaxed |
| Testing private methods | Brittle tests | Test through public API |
Thread.sleep() in tests | Flaky | Use awaitility or latch |
Quality Checklist
Command Reference (Copy-Paste Ready)
./gradlew :module-core-domain:test --tests "*ServiceTest"
./gradlew :module-core-domain:compileKotlin
./gradlew :module-core-domain:test
./gradlew build
./gradlew clean build