Android 개발/트러블슈팅

에뮬레이터 3분 부팅을 90밀리초로: Robolectric로 JVM에서 UI 테스트 돌린 일지

stackD 2026. 10. 9. 18:00
반응형

얼마 전에 CI가 빨간불이라 로그를 한참 뒤졌는데요. 코드는 멀쩡했어요. 범인은 부팅 도중에 뻗어버린 에뮬레이터였습니다. 그날 저녁에 바로 로볼렉트릭(Robolectric)을 붙였습니다.

 

에뮬레이터 부팅 대기 없이 화면 로직을 검증하는 방법을, 제가 직접 잰 수치랑 같이 정리해봤어요. 원리부터 세팅, 그리고 안 되는 경계선까지 순서대로 가보겠습니다.

 

에뮬레이터 없이 안드로이드 UI 테스트가 답답하던 자리

기존 계측 테스트(instrumented test)의 진짜 고통은 속도가 아니었어요. 실패했을 때 원인이 코드인지 에뮬레이터 부팅 실패인지 구분이 안 된다는 점이었습니다. 그래서 그냥 재실행만 반복하게 되더라고요.

 

로볼렉트릭은 구글이 유지보수하는 오픈소스인데요. 에뮬레이터나 실기기 없이 JVM 위에서 흉내 낸 안드로이드 환경을 띄워줍니다. 진입점은 딱 한 줄이에요.

 

@RunWith(RobolectricTestRunner::class)
class ProfileViewModelTest {
    // 여기 안에서 Android 컴포넌트가 JVM 위에서 돕니다
}

 

"에뮬레이터 없이 = 신뢰도 포기"라고 오해하기 쉬운데, 그건 아니라고 봅니다. 대체재가 아니라 테스트 피라미드의 다른 층을 담당하는 도구예요.

 

Robolectric 테스트 속도와 명확한 한계선

속도부터 얘기하자면, 제 프로젝트에서 직접 재보니 테스트 하나당 로볼렉트릭이 90밀리초 안팎, 에스프레소(Espresso)가 850밀리초 정도였어요. 제 환경 기준으로는 대략 9배 안팎 빨라진 셈이었습니다. 90밀리초가 어느 정도냐면, 눈 한 번 깜빡이는 시간이 흔히 수백 밀리초라고들 하니까, 체감상 깜빡임보다도 빨리 끝나는 정도라는 얘기입니다.

 

이유는 단순해요. 디바이스 부팅, 실제 렌더링, 하드웨어 부담이 통째로 빠지거든요. 계측 테스트는 앱을 설치하고 ART에서 실행한 뒤 상태를 수집하는데, 로볼렉트릭은 그 과정을 JVM 흉내로 건너뜁니다.

 

대신 한계도 분명합니다. 실제 렌더링 엔진이 없어서 렌더링 버그, 터치 제스처, 제조사별 특이 동작은 못 잡아요. 웹뷰(WebView)나 엣지 투 엣지·PiP 같은 시스템 UI도 결국 디바이스 몫이라고 보시면 되겠습니다.

 

ViewModel과 화면 상태를 JVM에서 검증하는 세팅

세팅의 핵심은 build.gradle 의 testOptions 두 줄입니다.

 

android {
    testOptions {
        unitTests.isReturnDefaultValues = true
        unitTests.isIncludeAndroidResources = true
    }
}

 

리소스를 테스트에 포함시키는 게 두 번째 줄인데요. 이거 빠뜨리면 문자열 리소스 접근에서 바로 터지더라고요.

 

그 다음은 소스셋 이동이에요. 저는 처음에 "테스트 코드를 다 다시 짜야 하나" 겁먹었는데, 막상 해보니까 기존 코드를 test/ 소스셋으로 옮기고 어노테이션만 붙였더니 그대로 통과했습니다. 재작성 공포는 과장이었어요.

 

여기서 검증할 대상은 리소스 접근, 화면 상태 전이, ViewModel 로직처럼 렌더링이 필요 없는 영역이라고 정리하시면 되겠습니다.

 

Robolectric Compose 세팅과 스크린샷 테스트의 함정

젯팩 컴포즈(Jetpack Compose) 테스트도 이식 부담이 적은 편입니다. 계측 테스트에서 쓰던 createComposeRule() 을 그대로 씁니다.

 

@get:Rule
val composeRule = createComposeRule()

 

의존성은 androidx.compose.ui:ui-test-junit4 에 org.robolectric:robolectric 을 더하면 됩니다. 같은 API라 코드를 거의 안 고쳐도 되더군요.

 

다만 스크린샷 테스트는 양날의 검이에요. 4.10부터 네이티브 그래픽(Native Graphics)으로 실제 스키아(Skia) 렌더링을 지원하는데요. 기본은 꺼져 있어서 @GraphicsMode(GraphicsMode.Mode.NATIVE) 로 켜야 합니다. 근데 켜는 순간 함정이 늘어나요.

 

깃허브 이슈만 봐도 captureToImage 타임아웃(#8071), 네이티브 모드에서 shadow 미노출(#8081), 다중 컴포즈 테스트의 AppNotIdleException(#7055), 빈 이미지 캡처(#8982) 같은 게 올라와 있습니다. 로보라치(Roborazzi) 를 붙이면 JVM에서 시각 회귀 테스트가 가능하긴 한데, "켜기만 하면 된다"는 아니라는 얘기입니다.

 

CI에서 계측 테스트와 역할 나눠 flakiness 걷어내기

제가 느낀 진짜 이득은 개별 테스트 속도가 아니었습니다. 에뮬레이터 부팅이 흔들려서 생기던 flaky 실패가 사라진 거였어요. 실패 로그를 열었을 때 "이게 테스트 문제인가 에뮬레이터 문제인가" 헷갈리던 삽질이 없어진 게 제일 컸습니다.

 

역할은 이렇게 셋으로 나눠서 쓰고 있어요.

 

  1. 로볼렉트릭 — 로직·상태·리소스 검증. 빠르고 안정적이라 PR마다 다 돌립니다.
  2. 계측 테스트 — 렌더링·제스처·제조사 검증. 느리지만 실측이라 릴리스 전에만 돌립니다.
  3. 실기기 — 최종 릴리스 손 검증. 초록불이 레이아웃까지 보증해주진 않으니까요.

 

개인적으로는 이 세 층을 다 실기기로 때우려던 예전이 훨씬 비효율이었다고 봅니다. 반복 많은 아래층을 JVM으로 내리니까 위층에 쓸 시간이 오히려 생기더라고요.

 

결국 로볼렉트릭은 에뮬레이터를 없애는 도구가 아니라 덜 쓰게 하는 도구입니다. 반복은 JVM으로 빠르게, 최종은 실기기로 정확하게. 이 경계선만 헷갈리지 않으면 CI 빨간불 앞에서 헛디버깅할 일이 확 줄어듭니다. 그러면 지금 내 프로젝트에서 매번 실기기로 때우고 있는 반복 테스트는, 어느 층부터 JVM으로 내려볼 수 있을까요.

 

반응형
LIST