Skip to content

[Feat] Roborazzi + Robolectric 기반 스크린샷 테스트 도입#476

Open
PeraSite wants to merge 21 commits intodevelopfrom
feat/screenshot-test
Open

[Feat] Roborazzi + Robolectric 기반 스크린샷 테스트 도입#476
PeraSite wants to merge 21 commits intodevelopfrom
feat/screenshot-test

Conversation

@PeraSite
Copy link
Member

@PeraSite PeraSite commented Feb 15, 2026

Summary

이번 PR에서 Roborazzi + Robolectric 기반의 스크린샷 회귀 테스트 체계를 도입해
Activity/Fragment/Compose(Route/Screen) 단위의 화면 변화를 자동 검증하도록 구성했습니다.

핵심 효과:

  • 스냅샷 테스트 대상 누락을 ScreenCoverageGuardTest로 강제
  • Map/WebView 같은 비결정 영역을 테스트 seam으로 고정해 flaky 감소
  • PR 단계에서 verifyRoborazziDebug로 회귀 자동 검증

변경 규모:

  • 113 files changed (+2,437 / -40)
  • 스크린샷 테스트 코드 12개 파일 추가
  • baseline 이미지 91장 추가
success_cafeteria 예시: MainActivity의 cafeteria 상태의 baseline 스크린샷

Describe your changes

As-is To-Be
UI 회귀 확인이 수동 중심 Roborazzi 기반 자동 회귀 검증 도입
일부 스냅샷이 실제 화면 구조를 충분히 반영하지 못함 Activity/Fragment/Compose 전반 실제 화면 상태 스냅샷 추가
누락된 화면이 있어도 테스트에서 바로 드러나지 않음 인벤토리 스캐너 + 커버리지 가드로 누락/stale 즉시 탐지
Map/WebView/Analytics 등 외부 의존으로 스냅샷 변동 가능 ScreenshotTestSeam + 결정성 규칙으로 렌더 결과 고정
CI에서 스크린샷 회귀를 강제하지 않음 PR workflow에 :app:verifyRoborazziDebug 추가

주요 구현 내용

  • Gradle/의존성

    • roborazzi 플러그인 및 roborazzi/robolectric/junit-vintage 의존성 추가
    • recordRoborazzi/verifyRoborazzi 실행 시에만 캡처되도록 시스템 프로퍼티 분기
    • 출력 경로를 app/src/test/screenshots, 비교 산출물 경로를 app/build/outputs/roborazzi로 정리
  • 결정성(Deterministic) 인프라

    • ScreenshotDeterminismRule로 Locale/Timezone/Animation scale 고정
    • ScreenshotTestSeam 추가
    • Map 화면은 테스트 모드에서 결정적 placeholder 렌더 사용
    • WebView는 테스트 모드에서 고정 HTML 렌더 사용
    • 테스트 모드에서는 screen view analytics 전송 스킵
  • 커버리지 가드

    • ScreenCoverageRegistry, ScreenTargetScanner, ScreenCoverageGuardTest 추가
    • 대상 누락/오래된 타겟/compose screen 분류 누락을 테스트에서 즉시 실패 처리
  • 스크린샷 테스트 스위트

    • ActivityScreenSnapshotsTest
    • FragmentScreenSnapshotsTest
    • ComposeRouteScreenshotsTest
    • 상태별 fixture(FakeUiStates, FakeScreenFixtures)로 loading/empty/success/error 케이스 캡처
  • CI/문서

    • .github/workflows/debug.yml에 PR용 screenshot-verify job 추가
    • 실패 시 roborazzi 산출물 artifact 업로드
    • docs/SCREENSHOT_TESTING.md 문서 추가 및 README 링크 연결

Test

  • ./gradlew :app:testDebugUnitTest --tests "com.eatssu.android.screenshot.inventory.ScreenCoverageGuardTest"
  • ./gradlew :app:verifyRoborazziDebug

Issue

To reviewers

  • ScreenCoverageRegistry에 현재 대상 화면/상태가 충분히 반영됐는지
  • 테스트 seam(ScreenshotTestSeam)이 프로덕션 동작에 영향 없이 테스트에서만 활성화되는지
  • baseline 이미지 변경이 의도된 UI 변화와 일치하는지
  • CI screenshot-verify 실패 시 artifact 확인 흐름이 적절한지

@PeraSite PeraSite changed the title Feat/screenshot test [Feat] Roborazzi + Robolectric 기반 스크린샷 테스트 도입 Feb 15, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @PeraSite, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 앱의 UI 안정성과 테스트 용이성을 크게 향상시키기 위해 포괄적인 스크린샷 회귀 테스트 프레임워크를 통합합니다. 이를 통해 개발 과정에서 시각적 회귀를 조기에 감지하고, 비결정적 UI 요소에 대한 테스트 신뢰도를 높이며, 광범위한 단위 테스트를 추가하여 전반적인 코드 견고성을 강화했습니다.

Highlights

  • 스크린샷 회귀 테스트 도입: Roborazzi와 Robolectric 기반의 스크린샷 회귀 테스트 체계를 도입하여 Activity, Fragment, Compose(Route/Screen) 단위의 화면 변화를 자동으로 검증합니다.
  • 테스트 결정성 확보: Map 및 WebView와 같은 비결정적 영역을 ScreenshotTestSeam을 통해 결정적 렌더링으로 고정하여 테스트의 안정성을 높였습니다. Locale, Timezone, Animation scale도 고정됩니다.
  • 스크린샷 커버리지 가드: ScreenCoverageGuardTest를 도입하여 스냅샷 테스트 대상 누락, 오래된 타겟, Compose 스크린 분류 누락 등을 자동으로 탐지하고 강제합니다.
  • CI/CD 통합 및 문서화: PR 워크플로우에 :app:verifyRoborazziDebug 작업을 추가하여 CI 단계에서 회귀를 자동 검증하고, 실패 시 Roborazzi 산출물을 아티팩트로 업로드합니다. 관련 가이드 문서(docs/SCREENSHOT_TESTING.md)도 추가되었습니다.
  • 테스트 코드 대폭 추가: 데이터 매퍼, 리포지토리, 유스케이스, ViewModel 등 다양한 계층에 걸쳐 Kotest BehaviorSpec 기반의 단위 테스트가 대량으로 추가되어 코드 품질과 안정성이 향상되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • README.md
    • 스크린샷 테스트 가이드 문서 링크를 추가했습니다.
  • app/build.gradle.kts
    • Roborazzi 플러그인 및 관련 의존성을 추가하고, 스크린샷 캡처를 위한 시스템 프로퍼티와 출력 경로를 설정했습니다.
  • app/src/main/java/com/eatssu/android/di/AppModule.kt
    • java.time.Clock 의존성을 추가했습니다.
  • app/src/main/java/com/eatssu/android/domain/usecase/alarm/AlarmUsecase.kt
    • 알람 스케줄링 로직에 Clock을 주입하여 테스트 용이성을 확보했습니다.
  • app/src/main/java/com/eatssu/android/presentation/map/MapFragmentView.kt
    • ScreenshotTestSeam을 활용하여 지도 화면의 비결정적 요소를 테스트 모드에서 결정적으로 렌더링하도록 수정했습니다.
  • app/src/main/java/com/eatssu/android/presentation/mypage/terms/WebViewActivity.kt
    • ScreenshotTestSeam을 활용하여 WebView의 내용을 테스트 모드에서 결정적 HTML로 렌더링하도록 수정했습니다.
  • app/src/main/java/com/eatssu/android/presentation/util/AnalyticsUtil.kt
    • 스크린샷 테스트 모드에서 분석 이벤트 로깅을 건너뛰도록 수정했습니다.
  • app/src/main/java/com/eatssu/android/presentation/util/CalendarUtil.kt
    • getNextDayDate 함수에 Clock 매개변수를 추가하여 테스트 용이성을 확보했습니다.
  • app/src/main/java/com/eatssu/android/presentation/util/ScreenshotTestSeam.kt
    • 스크린샷 테스트를 위한 결정적 렌더링 토글을 제어하는 유틸리티 객체를 추가했습니다.
  • app/src/main/java/com/eatssu/android/presentation/widget/WidgetCacheManager.kt
    • 위젯 캐시 관리 로직에 Clock을 주입하여 테스트 용이성을 확보했습니다.
  • app/src/main/java/com/eatssu/android/presentation/widget/util/WidgetDataDisplayManager.kt
    • 위젯 데이터 표시 관리 로직에 Clock을 주입하여 테스트 용이성을 확보했습니다.
  • app/src/test/java/com/eatssu/android/data/model/ApiResultBehaviorSpec.kt
    • ApiResult 확장 함수에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/dto/response/MenuAndMealResponseMapperBehaviorSpec.kt
    • 메뉴 및 식단 응답 매퍼에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/dto/response/PartnershipResponseMapperBehaviorSpec.kt
    • 제휴 응답 매퍼에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/dto/response/ReviewResponseMapperBehaviorSpec.kt
    • 리뷰 응답 매퍼에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/dto/response/UserAndTokenResponseMapperBehaviorSpec.kt
    • 사용자 및 토큰 응답 매퍼에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/paging/BaseReviewPagingSourceBehaviorSpec.kt
    • BaseReviewPagingSource에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/paging/MealReviewPagingSourceBehaviorSpec.kt
    • 식단 리뷰 페이징 소스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/paging/MenuReviewPagingSourceBehaviorSpec.kt
    • 메뉴 리뷰 페이징 소스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/FirebaseRemoteConfigRepositoryImplBehaviorSpec.kt
    • Firebase Remote Config 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/HealthCheckRepositoryImplBehaviorSpec.kt
    • 헬스 체크 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/MealRepositoryImplBehaviorSpec.kt
    • 식단 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/MenuRepositoryImplBehaviorSpec.kt
    • 메뉴 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/OauthRepositoryImplBehaviorSpec.kt
    • OAuth 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/PartnershipRepositoryImplBehaviorSpec.kt
    • 제휴 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/ReportRepositoryImplBehaviorSpec.kt
    • 신고 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/ReviewRepositoryImplBehaviorSpec.kt
    • 리뷰 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/data/remote/repository/UserRepositoryImplBehaviorSpec.kt
    • 사용자 리포지토리 구현체에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/di/network/ApiResultCallAdapterBehaviorSpec.kt
    • ApiResultCallAdapter에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/di/network/ApiResultCallAdapterFactoryBehaviorSpec.kt
    • ApiResultCallAdapterFactory에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/di/network/ApiResultCallBehaviorSpec.kt
    • ApiResultCall에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/di/network/TokenAuthenticatorBehaviorSpec.kt
    • 토큰 인증자에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/di/network/TokenInterceptorBehaviorSpec.kt
    • 토큰 인터셉터에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/alarm/AlarmNotificationStatusUseCasesBehaviorSpec.kt
    • 알람 알림 상태 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/alarm/AlarmUseCaseBehaviorSpec.kt
    • 알람 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/auth/AuthDelegatingUseCasesBehaviorSpec.kt
    • 인증 위임 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/auth/ReissueAndStoreTokenUseCaseBehaviorSpec.kt
    • 토큰 재발급 및 저장 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/health/HealthCheckUseCaseBehaviorSpec.kt
    • 헬스 체크 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/menu/GetMenuListUseCaseBehaviorSpec.kt
    • 메뉴 목록 조회 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/menu/GetValidMenusOfMealUseCaseBehaviorSpec.kt
    • 식단 유효 메뉴 조회 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/review/GetReviewInfoUseCaseBehaviorSpec.kt
    • 리뷰 정보 조회 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/review/ReviewDelegatingUseCasesBehaviorSpec.kt
    • 리뷰 위임 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/review/WriteReviewUseCaseBehaviorSpec.kt
    • 리뷰 작성 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/user/GetPartnershipDetailUseCaseBehaviorSpec.kt
    • 제휴 상세 조회 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/user/UserDelegatingUseCasesBehaviorSpec.kt
    • 사용자 위임 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/user/ValidateNicknameLocalUseCaseBehaviorSpec.kt
    • 닉네임 로컬 유효성 검사 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCaseBehaviorSpec.kt
    • 오늘의 식단 위젯 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/domain/usecase/widget/WidgetRestaurantUseCasesBehaviorSpec.kt
    • 위젯 식당 유스케이스에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/MainViewModelBehaviorSpec.kt
    • 메인 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/cafeteria/info/InfoViewModelBehaviorSpec.kt
    • 식당 정보 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/cafeteria/menu/MenuViewModelBehaviorSpec.kt
    • 메뉴 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewListViewModelBehaviorSpec.kt
    • 리뷰 목록 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModelBehaviorSpec.kt
    • 리뷰 수정 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/cafeteria/review/report/ReportViewModelBehaviorSpec.kt
    • 신고 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/cafeteria/review/write/WriteReviewViewModelBehaviorSpec.kt
    • 리뷰 작성 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/intro/IntroViewModelBehaviorSpec.kt
    • 인트로 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/login/LoginViewModelBehaviorSpec.kt
    • 로그인 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/login/UserApiClientBehaviorSpec.kt
    • 카카오 UserApiClient 확장 함수에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt
    • 지도 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/mypage/MyPageViewModelBehaviorSpec.kt
    • 마이페이지 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/mypage/SignOutViewModelBehaviorSpec.kt
    • 회원 탈퇴 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/mypage/language/LanguageSelectorViewModelBehaviorSpec.kt
    • 언어 선택 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModelBehaviorSpec.kt
    • 내 리뷰 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModelBehaviorSpec.kt
    • 사용자 정보 ViewModel에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/util/CalendarUtilBehaviorSpec.kt
    • CalendarUtil에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/util/TimeUtilBehaviorSpec.kt
    • TimeUtil에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/widget/WidgetCacheManagerBehaviorSpec.kt
    • 위젯 캐시 관리자에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/presentation/widget/util/WidgetDataDisplayManagerBehaviorSpec.kt
    • 위젯 데이터 표시 관리자에 대한 행동 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/compose/ComposeRouteScreenshotsTest.kt
    • Compose 라우트 및 스크린에 대한 스크린샷 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/core/RoborazziMode.kt
    • Roborazzi 테스트 모드를 정의하는 enum 클래스를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/core/ScreenshotCapture.kt
    • 스크린샷 캡처 유틸리티 객체를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/core/ScreenshotDeterminismRule.kt
    • 스크린샷 테스트를 위한 결정성 환경을 설정하는 JUnit 규칙을 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/core/ScreenshotTestApplication.kt
    • Robolectric 테스트를 위한 더미 Application 클래스를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/fixtures/FakeScreenFixtures.kt
    • 스크린샷 테스트를 위한 가짜 화면 데이터를 제공하는 객체를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/fixtures/FakeUiStates.kt
    • 스크린샷 테스트를 위한 가짜 UI 상태를 제공하는 객체를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/inventory/ScreenCoverageGuardTest.kt
    • 스크린샷 테스트 커버리지를 검증하는 가드 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/inventory/ScreenCoverageRegistry.kt
    • 스크린샷 테스트 커버리지 항목을 등록하는 레지스트리 객체를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/inventory/ScreenTargetScanner.kt
    • 스크린샷 테스트 대상을 스캔하는 유틸리티 객체를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/xml/ActivityScreenSnapshotsTest.kt
    • Activity 화면에 대한 스크린샷 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/screenshot/xml/FragmentScreenSnapshotsTest.kt
    • Fragment 화면에 대한 스크린샷 테스트를 추가했습니다.
  • app/src/test/java/com/eatssu/android/test/AppBehaviorSpec.kt
    • Kotest BehaviorSpec 테스트를 위한 기본 클래스를 추가했습니다.
  • app/src/test/java/com/eatssu/android/test/MainDispatcherListener.kt
    • 코루틴 테스트 디스패처를 설정하고 MockK 모의 객체를 정리하는 Kotest 리스너를 추가했습니다.
  • app/src/test/java/com/eatssu/android/test/TestFixtures.kt
    • 테스트에 사용되는 샘플 도메인 모델 데이터를 제공하는 객체를 추가했습니다.
  • app/src/test/java/com/eatssu/android/test/TestHelpers.kt
    • UI 이벤트 및 상태 테스트를 위한 헬퍼 함수를 추가했습니다.
  • build.gradle.kts
    • 루트 build.gradle.kts에 Roborazzi 플러그인을 추가했습니다.
  • core/common/build.gradle.kts
    • 단위 테스트에 JUnit Platform을 사용하도록 testOptions를 추가하고, Kotest, MockK, Coroutines Test, Turbine 의존성을 추가했습니다.
  • core/common/src/test/java/com/eatssu/common/UiContractBehaviorSpec.kt
    • UiStateUiEvent에 대한 행동 테스트를 추가했습니다.
  • core/common/src/test/java/com/eatssu/common/UiTextBehaviorSpec.kt
    • UiText 구현체에 대한 행동 테스트를 추가했습니다.
  • core/common/src/test/java/com/eatssu/common/enums/EnumsBehaviorSpec.kt
    • 다양한 enum 클래스에 대한 행동 테스트를 추가했습니다.
  • docs/SCREENSHOT_TESTING.md
    • 새로운 스크린샷 회귀 테스트 시스템에 대한 포괄적인 문서를 추가했습니다.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/debug.yml
Activity
  • PeraSite님이 Roborazzi + Robolectric 기반의 스크린샷 회귀 테스트 체계를 도입하고 Activity/Fragment/Compose(Route/Screen) 단위의 화면 변화를 자동 검증하도록 구성했습니다.
  • PeraSite님이 스냅샷 테스트 대상 누락을 ScreenCoverageGuardTest로 강제하고, Map/WebView 같은 비결정 영역을 테스트 seam으로 고정하여 flaky 테스트를 감소시켰습니다.
  • PeraSite님이 PR 단계에서 verifyRoborazziDebug로 회귀 자동 검증을 추가했습니다.
  • PeraSite님이 12개의 스크린샷 테스트 코드 파일과 91장의 baseline 이미지를 추가했습니다.
  • PeraSite님이 Gradle/의존성 설정을 업데이트하여 roborazzi 플러그인 및 관련 의존성을 추가하고, recordRoborazzi/verifyRoborazzi 실행 시에만 캡처되도록 시스템 프로퍼티를 분기했습니다.
  • PeraSite님이 ScreenshotDeterminismRule로 Locale/Timezone/Animation scale을 고정하고, ScreenshotTestSeam을 추가하여 Map/WebView 렌더링을 결정적으로 만들었습니다.
  • PeraSite님이 ScreenCoverageRegistry, ScreenTargetScanner, ScreenCoverageGuardTest를 추가하여 대상 누락/오래된 타겟/compose screen 분류 누락을 테스트에서 즉시 실패 처리하도록 했습니다.
  • PeraSite님이 ActivityScreenSnapshotsTest, FragmentScreenSnapshotsTest, ComposeRouteScreenshotsTest 등 스크린샷 테스트 스위트를 추가하고 상태별 fixture를 활용했습니다.
  • PeraSite님이 .github/workflows/debug.yml에 PR용 screenshot-verify job을 추가하고 실패 시 roborazzi 산출물 artifact를 업로드하도록 설정했습니다.
  • PeraSite님이 docs/SCREENSHOT_TESTING.md 문서를 추가하고 README에 링크를 연결했습니다.
  • PeraSite님이 ApiResult 확장 함수, 다양한 응답 매퍼, 페이징 소스, 리포지토리, 유스케이스, ViewModel 등 광범위한 영역에 걸쳐 Kotest BehaviorSpec 기반의 단위 테스트를 대량으로 추가했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 PR은 Roborazzi와 Robolectric을 이용한 스크린샷 테스트 체계를 도입하여 앱의 UI 안정성을 크게 향상시키는 중요한 변경입니다. 결정성 확보를 위한 테스트 seam 도입, 시간 관련 로직의 테스트 용이성 개선, 그리고 방대한 양의 단위 테스트 및 UI 테스트 추가 등 인상적인 작업입니다. 특히 커버리지 가드를 통해 테스트 누락을 방지하는 구조는 매우 훌륭합니다. 몇 가지 발견된 버그와 개선점에 대한 의견을 남겼으니 확인 부탁드립니다.

Comment on lines +131 to +144
`when`("writeMenuReview에서 likeMenuIdList가 빈 리스트면") {
then("현재 구현 그대로 NoSuchElementException이 발생한다") {
runTest {
shouldThrow<NoSuchElementException> {
repository.writeMenuReview(
rating = 1,
content = "x",
imageUrls = emptyList(),
likeMenuIdList = emptyList(),
)
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

이 테스트 케이스는 likeMenuIdList가 비어 있을 때 NoSuchElementException이 발생하는 버그를 잘 보여주고 있습니다. writeMenuReview 구현에서 빈 리스트에 .first()를 호출하는 것으로 보입니다. likeMenuIdList가 비어 있거나 null일 경우, 요청 시 menuLike 필드를 null로 설정하는 등 예외 상황을 안전하게 처리하도록 수정하는 것이 좋겠습니다.

Comment on lines +91 to +111
`when`("수정이 실패하면") {
val useCase2 = mockk<ModifyReviewUseCase>()
val viewModel = ModifyViewModel(useCase2)
viewModel.init(4, "old", likes)
viewModel.onContentChanged("new")
coEvery { useCase2(11L, 4, "new", any()) } returns false

then("현재 동작(characterization): 실패 토스트 후에도 뒤로가기+성공 토스트를 보낸다") {
runTest {
viewModel.uiEvent.test {
viewModel.submit(11L)
advanceUntilIdle()

awaitToastEvent().assertToast(R.string.toast_review_modify_failed, ToastType.ERROR)
awaitItem() shouldBe UiEvent.NavigateBack
awaitToastEvent().assertToast(R.string.toast_review_modify_success, ToastType.SUCCESS)
cancelAndIgnoreRemainingEvents()
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

이 테스트는 리뷰 수정 실패 시 실패 토스트와 성공 토스트가 모두 발생하는 버그를 발견했습니다. 실패 시에는 실패 처리 로직만 수행하고 성공 흐름으로 넘어가지 않도록 수정해야 합니다.

Comment on lines +10 to +14
@Volatile
private var forceEnabled: Boolean = false

val isEnabled: Boolean
get() = forceEnabled || System.getProperty(PROPERTY_KEY) == "true"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

@Volatile 필드인 forceEnabled를 사용하여 테스트 모드를 제어하는 방식이 다소 복잡하며, 잠재적인 동시성 문제를 야기할 수 있습니다. ScreenshotDeterminismRule에서 이미 시스템 프로퍼티를 설정하고 있으므로, forceEnabled를 제거하고 시스템 프로퍼티에만 의존하도록 단순화하는 것이 좋겠습니다. 이렇게 하면 정적 가변 상태를 제거하여 코드를 더 견고하고 예측 가능하게 만들 수 있습니다.

enableForTestdisableForTest에서도 forceEnabled 관련 코드를 제거해야 합니다.

Suggested change
@Volatile
private var forceEnabled: Boolean = false
val isEnabled: Boolean
get() = forceEnabled || System.getProperty(PROPERTY_KEY) == "true"
val isEnabled: Boolean
get() = System.getProperty(PROPERTY_KEY) == "true"

Comment on lines +213 to +252
`when`("이미지 업로드 URL이 null이어도 리뷰 작성이 성공하면") {
val viewModel = WriteReviewViewModel(writeReviewUseCase, getImageUrlUseCase, getValidMenusOfMealUseCase)
val context = mockk<Context>()
val resolver = mockk<ContentResolver>()
val uri = mockk<Uri>()
val cacheDir = createTempDir(prefix = "write-review-null-url")
val compressed = File(cacheDir, "compressed.jpg").apply { writeBytes(byteArrayOf(1, 2, 3)) }

every { context.contentResolver } returns resolver
every { context.cacheDir } returns cacheDir
every { resolver.openInputStream(uri) } returns ByteArrayInputStream(byteArrayOf(1, 2, 3))

mockkObject(Compressor)
coEvery { Compressor.compress(context, any()) } returns compressed
coEvery { getImageUrlUseCase(compressed) } returns null
coEvery {
writeReviewUseCase(MenuType.FIXED, 1L, 4, "", null, any())
} returns true
mockkObject(EventLogger)
every { EventLogger.completeReview(any(), any(), any()) } just Runs

then("현재 동작대로 이미지 업로드 성공 토스트 후 리뷰 성공 흐름을 유지한다") {
runTest {
viewModel.loadMenuList(MenuType.FIXED, 1L, "돈가스")
advanceUntilIdle()
viewModel.onRatingChanged(4)
viewModel.setSelectedImage(uri)

viewModel.uiEvent.test {
viewModel.postReview(MenuType.FIXED, 1L, context)
advanceUntilIdle()

expectToast(R.string.toast_image_upload_success, ToastType.SUCCESS)
expectToast(R.string.toast_review_write_success, ToastType.SUCCESS)
expectNavigateBack()
cancelAndIgnoreRemainingEvents()
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이미지 업로드에 실패하여 URL이 null로 반환되었음에도 '이미지 업로드 성공' 토스트가 표시되는 것은 사용자에게 혼란을 줄 수 있습니다. 이미지 URL을 받지 못한 경우는 실패로 간주하거나, 최소한 성공 토스트는 표시하지 않도록 수정하는 것이 좋겠습니다.

Comment on lines +535 to +585
`when`("단과대만 바꾼 상태로 저장하면") {
val setUserNicknameUseCase = mockk<SetUserNicknameUseCase>()
val getUserCollegeDepartmentUseCase = mockk<GetUserCollegeDepartmentUseCase>()
val setUserCollegeDepartmentUseCase = mockk<SetUserCollegeDepartmentUseCase>()
val validateNicknameServerUseCase = mockk<ValidateNicknameServerUseCase>()
val validateNicknameLocalUseCase = mockk<ValidateNicknameLocalUseCase>()
val userRepository = mockk<UserRepository>()

coEvery {
getUserCollegeDepartmentUseCase()
} returns sampleUserInfo(
nickname = "oldNick",
college = baseCollege,
department = baseDepartment,
)
coEvery { userRepository.getTotalColleges() } returns listOf(baseCollege, otherCollege)
coEvery { userRepository.getTotalDepartments(otherCollege.collegeId) } returns emptyList()
coEvery { userRepository.getTotalDepartments(baseCollege.collegeId) } returns listOf(baseDepartment)
every { validateNicknameLocalUseCase(any(), any(), any()) } returns NicknameValidationResult.Valid

val viewModel = UserInfoViewModel(
setUserNicknameUseCase = setUserNicknameUseCase,
getUserCollegeDepartmentUseCase = getUserCollegeDepartmentUseCase,
setUserCollegeDepartmentUseCase = setUserCollegeDepartmentUseCase,
validateNicknameServerUseCase = validateNicknameServerUseCase,
validateNicknameLocalUseCase = validateNicknameLocalUseCase,
userRepository = userRepository,
)

then("현재 동작대로 Loading 상태에서 조기 종료된다") {
runTest {
eventually(2.seconds) {
(viewModel.uiState.value is UiState.Success) shouldBe true
}

viewModel.selectCollege(otherCollege)
advanceUntilIdle()

viewModel.uiEvent.test {
viewModel.saveUserInfo()
advanceUntilIdle()

expectNoEvents()
cancelAndIgnoreRemainingEvents()
}

viewModel.uiState.value shouldBe UiState.Loading
coVerify(exactly = 0) { userRepository.setUserDepartment(any()) }
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

단과대만 변경하고 학과를 선택하지 않은 상태에서 저장을 시도하면 ViewModel이 Loading 상태에 멈추는 현상이 테스트를 통해 확인되었습니다. 이 경우, 저장 버튼을 비활성화하거나 사용자에게 학과를 선택해야 한다는 안내를 제공하여 UI가 멈추지 않도록 처리해야 합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

스크린샷 테스트 도입 및 CI 검증 체계 구축

1 participant

Comments