kotlin-testing
Kotlin testing with JUnit 5, MockK, Spring test slices, and Testcontainers. Provides mocking patterns, test slice selection, and TDD workflow for Spring Boot + Kotlin. Use when writing tests for Kotli
kotlin
testing
SKILL.md.claude/skills/kotlin-testing
GitHubKotlin 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 # 60-120초 낭비!
./gradlew test # 전체 테스트 금지!
./gradlew clean # clean 불필요!
# ✅ 올바른 사용
./gradlew :module-core-domain:test --tests "*ServiceTest" # 5-10초
./gradlew :module-server-api:test --tests "*ControllerTest" # 5-10초
JetBrains MCP 먼저 사용 (STEP 1)
코드 수정 후 항상 IDE 검사 먼저 실행:
# 필수: 코드 수정 후 즉시 에러 확인 (0-2초)
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 buildduring 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
# project-basecamp-server modules
:module-core-common:test # Utilities
:module-core-domain:test # Domain services, entities
:module-core-infra:test # Repository impls, external clients
:module-server-api:test # Controllers
Single Test Patterns
# Run single test class
./gradlew :module-core-domain:test --tests "*PipelineServiceTest"
# Run single test method (partial match)
./gradlew :module-core-domain:test --tests "*PipelineServiceTest.should create*"
# Run tests matching pattern
./gradlew :module-core-domain:test --tests "*Service*"
# Run with console output (for debugging)
./gradlew :module-core-domain:test --tests "*ServiceTest" --info
# Re-run only failed tests
./gradlew :module-core-domain:test --tests "*ServiceTest" --rerun
Incremental Build (NO clean!)
# Compile only (fastest check for syntax errors)
./gradlew :module-core-domain:compileKotlin
# Compile + test single class (fast TDD cycle)
./gradlew :module-core-domain:compileKotlin && \
./gradlew :module-core-domain:test --tests "*ServiceTest"
# Module build (no clean)
./gradlew :module-core-domain:build
# Full build WITHOUT clean (uses cache)
./gradlew build
JetBrains MCP Integration (Fastest)
# Option 1: Quick compile check via IDE inspection
jetbrains.get_file_problems(
filePath="module-core-domain/src/main/kotlin/.../Service.kt",
errorsOnly=True,
timeout=5000
)
# Option 2: Run test via terminal
jetbrains.execute_terminal_command(
command="./gradlew :module-core-domain:test --tests '*ServiceTest'",
timeout=60000,
maxLinesCount=200
)
# Option 3: Use existing run configuration
jetbrains.get_run_configurations() # List available
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
# Fastest: IDE inspection (0-2s)
jetbrains.get_file_problems(filePath="...", errorsOnly=True)
# Or: Gradle compile (3-5s)
./gradlew :module-core-domain:compileKotlin
3. Run Single Test (GREEN)
# Single test class (~5-10s)
./gradlew :module-core-domain:test --tests "*ServiceTest"
# Or via JetBrains
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
# After TDD cycle complete, verify module (~15-30s)
./gradlew :module-core-domain:test
# Only use full build for final CI check
./gradlew build # NO clean unless cache issues!
MCP Workflow
# 1. Check file for compile errors (FAST - no build)
jetbrains.get_file_problems(
filePath="module-core-domain/src/test/kotlin/.../ServiceTest.kt",
errorsOnly=True
)
# 2. Find existing test patterns
serena.search_for_pattern(
"@Test|@MockK|@SpringBootTest",
relative_path="project-basecamp-server/src/test/",
context_lines_after=1,
max_answer_chars=3000
)
# 3. Run specific test via JetBrains
jetbrains.execute_terminal_command(
command="./gradlew :module-core-domain:test --tests '*ServiceTest' --info",
timeout=60000
)
# 4. Spring testing docs
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`() {
// Arrange
val command = CreatePipelineCommand(name = "test")
val entity = PipelineEntity(id = 1, name = "test")
every { repository.save(any()) } returns entity
// Act
val result = service.create(command)
// Assert
assertThat(result.name).isEqualTo("test")
verify(exactly = 1) { repository.save(any()) }
}
}
Relaxed Mocks (Stubs)
@MockK(relaxed = true) // Returns sensible defaults
private lateinit var logger: Logger
// Or inline
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
- Unit tests use no Spring context (fast ~1s)
- Single test runs in <10 seconds
- Integration tests use appropriate slice
- Mocks verified with
verifyblocks - Testcontainers for database tests
- No
Thread.sleep()(use awaitility) - Tests follow AAA pattern (Arrange-Act-Assert)
- Test names describe behavior:
should X when Y
Command Reference (Copy-Paste Ready)
# TDD Fast Feedback (use these!)
./gradlew :module-core-domain:test --tests "*ServiceTest"
./gradlew :module-core-domain:compileKotlin
# Module verification
./gradlew :module-core-domain:test
# Final CI (only when needed)
./gradlew build
./gradlew clean build # Only if cache issues!