Maestro 는 mobile.dev 가 만든 모바일 UI 자동화, E2E 테스트 도구다. 사용자 흐름을 YAML 파일로 적어두면 에뮬레이터나 시뮬레이터에서 그대로 재생해준다. 앱 코드나 네이티브 빌드 설정을 바꾸지 않고, 빌드된 앱만 있으면 바로 돌릴 수 있다.
React Native 프로젝트에 Maestro 를 붙여서 실제로 운영할 때 필요한 설정과 명령, 자주 걸리는 문제를 정리한다.
curl -fsSL "https://get.maestro.mobile.dev" | bash
# 또는 Homebrew
brew tap mobile-dev-inc/tap
brew install mobile-dev-inc/tap/maestro
Java 17 이상이 필요하고, JAVA_HOME 이 해당 버전을 가리키고 있어야 한다. 설치 후 maestro --help 로 확인하고, maestro list-devices 로 연결된 에뮬레이터와 시뮬레이터 목록을 볼 수 있다.
Maestro 는 React Native 의 testID 를 id 선택자로 읽는다. 테스트에서 다룰 요소에 먼저 testID 를 붙인다.
<TextInput testID="login_email_input" placeholder="이메일" />
<TextInput testID="login_password_input" placeholder="비밀번호" secureTextEntry />
<Pressable testID="login_submit_button" onPress={handleLogin}>
<Text>로그인</Text>
</Pressable>
# .maestro/login.yaml
appId: com.example.app
---
- launchApp:
clearState: true
- tapOn:
id: "login_email_input"
- inputText: "test@example.com"
- tapOn:
id: "login_password_input"
- inputText: "password1234"
- hideKeyboard
- tapOn:
id: "login_submit_button"
- assertVisible:
id: "home_screen"
--- 위는 flow 설정, 아래는 실행할 명령 목록이다.appId 에는 Android 패키지 이름이나 iOS 번들 ID 를 적는다.clearState: true 로 매번 앱 데이터를 지운 상태에서 시작해야 이전 실행의 로그인 상태 때문에 결과가 달라지지 않는다.hideKeyboard 를 넣어두는 편이 안전하다.maestro test .maestro/login.yaml
# 기기가 여러 대 연결돼 있으면 대상 지정 (test 앞에 위치)
maestro --device emulator-5554 test .maestro/login.yaml
# flow 를 작성하는 동안 저장할 때마다 다시 실행
maestro test -c .maestro/login.yaml
-c 는 파일을 저장할 때마다 flow 를 다시 돌려주는 모드라서, 새 flow 를 만들 때 켜두면 작업 속도가 많이 빨라진다.
- tapOn:
id: "product_item_42"
- tapOn: "장바구니 담기" # 화면에 보이는 텍스트로 찾기
- assertVisible:
id: "cart_badge"
- assertNotVisible: "품절"
Maestro 는 요소가 나타날 때까지 기본적으로 기다리기 때문에 sleep 을 넣을 필요가 없다. 기본 대기 시간으로 부족한 화면만 따로 처리한다.
# 로딩이 긴 화면
- extendedWaitUntil:
visible:
id: "home_screen"
timeout: 10000
# 전환 애니메이션이 끝난 뒤에 진행
- waitForAnimationToEnd
목록 아래쪽 요소는 scrollUntilVisible 로 찾을 때까지 스크롤한다. 기본 방향은 아래, 기본 제한 시간은 20초다.
- scrollUntilVisible:
element:
id: "product_item_42"
direction: DOWN
팝업처럼 뜰 때도 있고 안 뜰 때도 있는 화면은 when 조건으로 처리한다. 조건이 맞지 않으면 실패하지 않고 건너뛴다.
- runFlow:
when:
visible: "나중에 하기"
commands:
- tapOn: "나중에 하기"
- runFlow:
when:
platform: Android
file: subflows/android-permissions.yaml
권한 팝업은 launchApp 에서 미리 정해두면 flow 에서 따로 처리하지 않아도 된다. Android 는 android.permission.* 전체 이름, iOS 는 notifications 같은 짧은 이름을 쓴다.
- launchApp:
clearState: true
permissions:
all: deny
notifications: allow
- takeScreenshot: checkout_done
중요한 단계마다 찍어두면 CI 에서 실패했을 때 어느 화면에서 멈췄는지 바로 확인할 수 있다.
로그인처럼 여러 flow 에서 반복되는 단계는 subflow 로 빼고 runFlow 로 불러온다.
# .maestro/checkout.yaml
appId: com.example.app
---
- runFlow: subflows/login.yaml
- tapOn:
id: "tab_cart"
계정 정보처럼 flow 에 직접 적으면 안 되는 값은 실행할 때 넘긴다.
- inputText: ${EMAIL}
maestro test -e EMAIL=test@example.com -e PASSWORD=secret .maestro/login.yaml
.maestro/
├── config.yaml
├── login.yaml
├── checkout.yaml
└── subflows/
├── login.yaml
└── android-permissions.yaml
# .maestro/config.yaml
flows:
- '*'
* 는 최상위 파일만 대상으로 하기 때문에 subflows 안의 파일이 단독으로 실행되지 않는다. 폴더를 넘기면 대상 flow 를 한 번에 실행한다.
flow 상단에 tags 를 달아두면 PR 마다 돌리는 스모크 테스트와 배포 전 전체 테스트를 나눠 실행할 수 있다.
appId: com.example.app
tags:
- smoke
---
maestro test --include-tags=smoke .maestro
텍스트 선택자는 처음 쓰기엔 편하지만 문구가 바뀌거나 다국어를 적용하면 바로 깨진다. 오래 유지할 flow 는 testID 기준으로 작성하고, 텍스트 선택자는 시스템 팝업처럼 testID 를 붙일 수 없는 곳에만 쓴다.
iOS 에서 터치 가능한 컴포넌트가 여러 겹으로 감싸져 있으면, 바깥 컴포넌트가 하나의 접근성 요소로 묶이면서 안쪽 요소를 찾지 못하는 경우가 있다. 바깥 컴포넌트에 accessible={false} 를 주면 안쪽 요소가 따로 잡힌다.
디버그 빌드에서는 LogBox 경고 배너나 에러 화면이 버튼을 가려서 테스트가 실패하기도 하고, Metro 가 떠 있어야 앱이 동작한다. 로컬에서 flow 를 만들 때는 디버그 빌드로 충분하지만, CI 에서는 릴리즈 빌드로 돌린다.
inputText 는 Android 에서 유니코드 문자를 지원하지 않는다. 한글 검색어나 이름을 입력하는 단계는 Android 에서 그대로 동작하지 않으니, 테스트 데이터는 영문과 숫자로 맞춘다. 한글 입력이 꼭 필요한 화면은 딥링크나 launchApp 의 arguments 로 값을 미리 넣어두는 방식으로 우회한다.
--debug-output: 실패 시점의 스크린샷과 로그를 지정한 폴더에 남긴다.maestro test --debug-output ./maestro-debug .maestro
CI 에서는 에뮬레이터를 띄우고 릴리즈 빌드를 설치한 뒤 실행한다. 결과는 JUnit 형식으로 남겨두면 CI 의 테스트 리포트 화면에서 바로 볼 수 있다.
maestro test \
--include-tags=smoke \
--format=JUNIT \
--output=maestro-report.xml \
--debug-output=./maestro-debug \
.maestro
기기를 여러 대 띄울 수 있다면 --shards 로 flow 를 나눠 병렬로 돌릴 수 있다. 에뮬레이터 관리가 부담스럽다면 mobile.dev 의 클라우드 실행 서비스를 쓰는 방법도 있다.
testID 가 id 선택자로 매핑되니, 다룰 요소에 testID 를 먼저 붙이고 텍스트 선택자는 최소한으로 쓴다.clearState 로 매번 같은 상태에서 시작하고, 권한은 launchApp 에서 미리 정해둔다.sleep 대신 extendedWaitUntil 과 waitForAnimationToEnd 를 쓴다.-e 환경 변수로 분리하고, tags 로 스모크 테스트를 나눈다.--debug-output 을 남겨둔다. Android 에서는 한글 입력이 안 된다는 점을 감안해 테스트 데이터를 준비한다.