이 문서는 RECAP 프로젝트의 테스트 작성/검증 기준을 정의한다.
- 현재 프로젝트는
:app,:core:*,:feature:*의 대상 모듈에 로컬 단위 테스트를 두는 멀티모듈 테스트 체계를 운영한다. - 로컬 단위 테스트는 JUnit5 중심으로 작성한다.
- 기존 JUnit4 테스트는 JUnit Vintage engine으로 당분간 함께 실행한다.
- Compose UI test, AndroidX test 의존성은 설정되어 있다.
- MockK, Turbine, coroutines-test, Room testing 의존성이 설정되어 있다.
- Compose Preview Screenshot Testing은
:app,:core:design및 UI가 있는:feature:*모듈에 적용되어 있다.
현재 설정된 주요 테스트 의존성:
- JUnit5:
org.junit.jupiter:junit-jupiter - JUnit Platform Launcher:
org.junit.platform:junit-platform-launcher - JUnit Vintage:
org.junit.vintage:junit-vintage-engine - MockK:
io.mockk:mockk - Coroutines test:
org.jetbrains.kotlinx:kotlinx-coroutines-test - Turbine:
app.cash.turbine:turbine - Room testing:
androidx.room:room-testing
Instrumentation/Compose UI test는 AndroidX Compose test가 JUnit4 rule 기반이므로 별도 정책을 유지한다.
- Screenshot Test의 추가·기준 이미지 갱신·실행은 사용자가 명시적으로 요청한 경우에만 수행한다. 레이아웃 변경이나 기존 screenshot test 존재만으로 자동 실행하지 않는다.
- 플러그인:
com.android.compose.screenshot(libs.plugins.compose.screenshot,0.0.1-alpha16) gradle.properties와 모듈android.experimental.enableScreenshotTest로screenshotTestsource set을 켠다- Preview는
src/screenshotTest/...에 두고@Preview+@PreviewTest로 지정한다 - 화면 크기·fontScale 매트릭스는
@QaPhoneMatrix(com.chalkak.recap.core.design.qa)로 적용한다 —docs/qa/GUIDE.md§3 - 의존성:
screenshotTestImplementation(libs.screenshot.validation.api),screenshotTestImplementation(libs.androidx.compose.ui.tooling) - 기준 이미지 갱신:
.\gradlew.bat updateDebugScreenshotTest - 검증:
.\gradlew.bat validateDebugScreenshotTest
PowerShell debug build:
$env:GRADLE_USER_HOME="$env:USERPROFILE\.gradle"; .\gradlew.bat assembleDebugqa 빌드(Performance 플러그인 instrumentation 포함):
$env:GRADLE_USER_HOME="$env:USERPROFILE\.gradle"; .\gradlew.bat assembleQaFirebase Observability 수동 확인(qa/release 기기):
- DEBUG: Crashlytics/Performance collection OFF
- qa/release: Crashlytics·Performance ON
- 강제 non-fatal / organize custom trace가 Firebase 콘솔에 보이는지 확인
전체 debug 로컬 단위 테스트:
$env:GRADLE_USER_HOME="$env:USERPROFILE\.gradle"; .\gradlew.bat testDebugUnitTest- production 코드는 대상 모듈의
src/main/java/...에 둔다. - 로컬 단위 테스트는 대상 모듈의
src/test/java/...또는src/test/kotlin/...에 둔다. - Android instrumentation / Compose UI 테스트는 대상 모듈의
src/androidTest/java/...또는src/androidTest/kotlin/...에 둔다. - Preview Screenshot 테스트는 screenshot plugin을 적용한 대상 모듈의
src/screenshotTest/java/...또는src/screenshotTest/kotlin/...에 둔다. - 패키지 경로는 대상 클래스와 동일하게 미러링한다.
예:
feature/home/src/main/java/com/chalkak/recap/feature/home/HomeViewModel.kt
feature/home/src/test/java/com/chalkak/recap/feature/home/HomeViewModelTest.kt
- production 코드 변경: 영향받는 모듈의
testDebugUnitTest와 루트assembleDebug를 실행한다. 영향 범위가 여러 모듈이거나 불명확하면 루트testDebugUnitTest를 실행한다. - 테스트 코드만 변경: 해당 모듈의 관련 테스트를 실행한다. production compile 영향이 있으면
assembleDebug도 실행한다. - Gradle plugin/dependency/build type 변경: 관련 테스트와 영향받는 build를 실행하며, qa/release 전용 설정을 바꿨다면 해당 variant도 검증한다.
- 문서만 변경: Gradle 빌드·테스트는 생략할 수 있으며 문서 링크, 명령, 현재 코드 사실을 정적으로 확인한다.
- 실행할 수 없는 필수 검증이 있으면 생략하지 않은 것처럼 보고하지 말고 원인과 영향 범위를 완료 응답에 남긴다.
- 레이아웃, 간격, typography,
core/design템플릿 또는 화면 구조 변경은 관련 Compose Preview를 확인한다. validateDebugScreenshotTest, 기준 이미지 갱신, screenshot test 추가·수정은 사용자가 명시적으로 요청한 경우에만 수행한다. 요청되지 않았다면 미실행 사유를 따로 보고할 필요가 없다.- 작은 화면·고배율·시스템 navigation inset에 영향을 줄 수 있는 변경(풀뷰포트, pinned bottom CTA, 고정 높이/패딩,
WindowInsets/system bar 처리)은docs/qa/GUIDE.md의 Emulator A(gesture)와 B(3-button)를 모두 확인한다. - 위 위험과 무관한 시각 변경은 VM A/B 검증을 생략할 수 있지만, 완료 응답 또는 3단 handoff Result에 생략 사유를 기록한다.
- 문서만 변경한 경우 레이아웃 QA는 생략한다.
- 파일명은
<대상클래스>Test.kt로 쓴다. - 테스트 함수명은 백틱으로 감싼 영어 서술형을 쓴다.
- Given/When/Then 주석은 쓰지 않고 빈 줄로 구획한다.
- 테스트 하나는 하나의 동작만 검증한다.
@Test
fun `onAction updates selected tab`() {
val viewModel = HomeViewModel()
viewModel.onAction(HomeAction.SelectTab(HomeTab.Recent))
assertEquals(HomeTab.Recent, viewModel.uiState.value.selectedTab)
}@Test는org.junit.jupiter.api.Test를 사용한다.@BeforeEach,@AfterEach도 JUnit5 패키지를 사용한다.- 예외 검증은
org.junit.jupiter.api.assertThrows를 사용한다. - Android instrumentation / Compose UI test는 JUnit4 기반 rule을 사용할 수 있다.
@Test
fun `throws when capture id is invalid`() {
assertThrows<IllegalArgumentException> {
require(captureId > 0)
}
}- 기본은 엄격 모크인
mockk<T>()를 사용한다. - 동기 함수는
every { } returns와verify { }를 사용한다. - suspend 함수는
coEvery { } returns와coVerify { }를 사용한다. - 반환값이 테스트와 무관한 부수 의존성에만
relaxed = true를 예외적으로 쓴다.
val repository = mockk<ScreenshotCardRepository>()
coEvery { repository.deleteAllCards() } returns Unit
viewModel.resetScreenshotData()
coVerify(exactly = 1) { repository.deleteAllCards() }- suspend 코드는
kotlinx.coroutines.test.runTest안에서 실행한다. viewModelScope처럼Dispatchers.Main을 참조하는 대상은 테스트마다 Main dispatcher를 지정하고 해제한다.- 비동기 작업 완료가 필요하면
advanceUntilIdle()을 사용한다.
@OptIn(ExperimentalCoroutinesApi::class)
class SomeViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@BeforeEach
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loads initial state`() = runTest(testDispatcher) {
viewModel.load()
advanceUntilIdle()
assertEquals(expected, viewModel.uiState.value)
}
}StateFlow와Flow의 방출 순서는 Turbine으로 확인한다.- Compose
mutableStateOf프로퍼티는 Flow가 아니므로 호출 후 값을 직접 읽어 검증한다. - 무한 Flow는 테스트 마지막에
cancelAndIgnoreRemainingEvents()를 호출한다.
viewModel.uiState.test {
assertEquals(initial, awaitItem())
viewModel.onAction(action)
assertEquals(expected, awaitItem())
cancelAndIgnoreRemainingEvents()
}- ViewModel 테스트
- UiState 초기값
- Action 처리 결과
- loading/error/empty state 전이
- 순수 domain/model 테스트
- validation
- mapper
- business rule
- Repository 테스트
- fake data source 기반
- 외부 API 실패와 retry 가능 상태
- Room DAO 테스트
- insert/query/update/delete
- migration 추가 시 migration test
- Compose UI 테스트
- 핵심 화면의 state별 렌더링
- 주요 버튼 action
codex-plan → cursor-implement → codex-review 3단 워크플로우를 사용한 경우에만 docs/handoff/HANDOFF.md의
Result 섹션에 결과를 남긴다. 그 밖의 작업은 HANDOFF.md를 읽거나 수정하지 않고 작업 완료 응답에 같은 수준의 변경·검증·미검증 사항을 보고한다.
## Cursor Result
- Changed files: ...
- Build/test: .\gradlew.bat testDebugUnitTest GREEN, .\gradlew.bat assembleDebug GREEN
- Design QA: not applicable - no layout change
- Open questions: none문서만 수정한 경우:
## Cursor Result
- Changed files: docs/TESTING.md
- Build/test: not run - docs only; links/commands/current facts checked
- Design QA: not applicable - docs only
- Open questions: none