UIAutomator는 안드로이드에서 UI를 테스트할 수 있게 도와주는 라이브러리입니다. 일반적으로 UI를 테스트하는 라이브러리는 아래와 같이 두개이며, 각각 특징이 다릅니다. 테스트하려는 용도에 적합한 것을 사용하면 됩니다.
- UIAutomator : 블랙박스 테스트, 두개 이상의 앱을 연동하여 테스트할 때 좋음
- Espresso : 화이트박스 테스트, 한개의 앱만 테스트할 때 좋음
이 두개의 가장 큰 차이점으로 Espresso는 whitebox 테스트입니다. 테스트 코드는 자신의 앱의 액티비티를 직접 호출시키고, 앱이 어떤 인텐트를 누구에게 전달하는지 가로챌 수 있어, 앱 구현이 잘되었는지 테스트할 수 있습니다.
반대로, UIAutomator는 블랙박스 테스트입니다. 앱의 구현이 어떻게 되었는지는 모릅니다. 앱 이름으로 앱을 실행시키고, 화면이 떳을 때 어떤 버튼을 누르고, 버튼을 눌렀을 때 어떤 팝업이 뜨는지 테스트하는 것입니다. 사용자는 시나리오 위주로 테스트 코드를 작성하는 것입니다.
테스트 코드로 버튼을 누를 수 있는 이유는 화면에 텍스트 정보와 버튼 ID 정보를 읽을 수 있기 때문입니다. 그렇기 때문에 두개 이상의 앱을 연동하여 테스트할 때 사용할 수 있습니다. (Espresso의 경우 1개의 앱만 테스트할 수 있습니다.)
UIAutomator를 사용하여 UI를 테스트하는 방법을 간단히 소개하겠습니다.
최신 버전은 UIAutomator2입니다. 최신 라이브러리 위주로 소개하겠습니다. 이 글에서 사용하는 코드는 모두 코틀린으로 작성되었습니다.
프로젝트 설정
안드로이드 스튜디오에서 프로젝트를 하나 만드세요. 앱을 구현하신 후에, UIAutomator를 이용하여 테스트 코드를 작성하려면 gradle에 라이브러리를 추가해야 합니다.
아래와 같이 앱 gradle의 dependency에 추가하시면 됩니다. 저는 Androidx 라이브러리를 사용하였습니다.
이 라이브러리는 Junit에서 동작하는 것이기 때문에 junit
과 androidx.test:runner
는 당연히 추가되어 있어야 합니다.
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
....
}
테스트 코드 작성
앱을 구현하셨다면 이제 테스트 코드를 작성해야 합니다.
테스트 코드는 프로젝트의 ../app/src/androidTest/
의 InstrumentedTest 파일에 작성해야 합니다.(Unit test에 작성하시면 안됩니다.)
위에서 설명한 것처럼 UIAutomator는 블랙박싱 테스트입니다. 마치 눈으로 화면을 보고 버튼을 터치하고, 어떤 텍스트가 보이는지 테스트하는 것과 같습니다. 이렇게 사람이 할 일을 자동화하는 것입니다.
우리는 이런 시나리오를 UIAutomator를 이용하여 코드로 옴겨야 합니다.
앱 실행하기
먼저 Home 버튼을 누르고 내 앱을 실행시키는 것을 코드로 작성할 것입니다.
여기서 UIAutomator의 UiDevice
, Until
, By
클래스를 사용합니다.
대략적인 사용방법을 알고, 자세한 것은 나중에 알아보겠습니다.
@Test
fun testRunMyApp() {
// Initialize UiDevice instance
val device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()); // 1
// Press home button
device.pressHome() // 2
// Wait for launcher
val launcherPackage = device.launcherPackageName // 3
assertThat(launcherPackage, notNullValue())
device.wait(
Until.hasObject(By.pkg(launcherPackage), 5000
) // 4
// Launch my app
val context = InstrumentationRegistry.getTargetContext()
val intent = context.packageManager
.getLaunchIntentForPackage(context.packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent) // 5
// Wait for the app to appear
device.wait(
Until.hasObject(By.pkg(context.packageName)), 5000
) // 6
}
'// 1'
처럼 중요한 것은 번호로 표시하였고 아래 설명하였습니다.
- UiDevice는 디바이스 정보에 접근할 수 있는 객체입니다. Home, Back 버튼을 누르거나, 특정 좌표를 터치할 수 있습니다.
또는 디바이스를 wakeUp, sleep 상태로 만들 수도 있습니다. 2. Home 버튼을 눌러 Launcher로 이동합니다. 3. launcherPackageName는 기본으로 설정된 Launcher의 이름을 가져옵니다. 4. wait은 어떤 상황을 기다립니다. 이 코드는 Launcher가 실행되기를 기다립니다. timeout은 5000ms로 설정했습니다. 5. 내 앱의 패키지 이름으로 인텐트를 만들어 실행시킵니다. 6. 내 앱이 실행되기를 기다립니다.
By (선택자)
위에서 Launcher 또는 내 앱을 찾을 때 By
클래스를 사용했습니다.
By
는 패키지, 클래스, 리소스 ID, 리소스 속성 등으로 앱의 구성 요소를 찾는데 사용합니다.
By
는 찾으려고 하는 객체 정보를 담고 있는 BySelector
객체를 리턴하며, 이것으로 대상이 무엇인지 찾는데 사용합니다.
예를 들어, 아래 코드에서 By.pkg
는 유튜브 앱에 대한 BySelector
를 리턴합니다.
By.pkg("com.android.youtube")
- By.pkg : 인자로 전달된 패키지에 해당하는 BySelector 리턴
- By.text : 화면에 보이는 텍스트에서 인자로 전달된 text에 해당하는 객체에 대한 BySelector 리턴
- By.clazz : 클래스이름으로 객체를 찾고 그 객체에 대한 BySelector를 리턴
- By.res : 리소스 ID로 객체를 찾고 그 객체에 대한 BySelector를 리턴
등등, 이외에도 많은 API를 제공합니다.
By가 제공하는 더 많은 메소드들은 By 레퍼런스를 참고해주세요.
Until
Until 클래스는 UiObject2Condition
객체를 리턴해주는 팩토리 클래스입니다.
UiObject2Condition
는 아래 코드처럼, wait()의 인자로 사용되며, 어떤 상황을 표현하는 객체입니다.
Until.hasObject
는 현재 디바이스가 인자로 받은 BySelector의 조건을 충족하는지 체크하는 코드입니다.
device.wait(
Until.hasObject(By.pkg(launcherPackage)), 5000
)
정리하면, Until은 어떤 상황을 표현하는 UiObject2Condition 객체를 만들어주고, 이 객체는 wait등과 함께 사용됩니다.
Until은 다음과 같이 여러 메소드들을 제공합니다.
- Until.hasObject(BySelector selector) : 디바이스의 요소들 중에서 인자로 전달된 선택자의 조건을 충족시키는 요소가 1개라도 있는지
- Until.longClickable(boolean isLongClickable) : 롱클릭이 가능한 요소가 있는지
- Until.scrollFinished(Direction direction) : 스크롤이 끝났는지
- Until.textEquals(String text): 인자로 전달된 텍스트와 일치하는 텍스트들이 화면에 있는지
Util이 제공하는 더 많은 메소드들은 Until 레퍼런스를 참고해주세요.
버튼을 찾고, 누르고, 팝업 확인하기
지금까지 앱을 실행하는 방법을 알아보았습니다.
그 과정에서 UI 요소를 의미하는 BySelector
와 어떤 상황의 조건을 표현하는 UiObject2Condition
의 개념에 대해서 알아보았습니다.
Selector를 이용하면 화면에 떠 있는 UI를 찾을 수 있습니다.
샘플로 OK 버튼을 누르면 Dialog가 뜨고, OK버튼을 누르면 Dialog가 종료되는 앱을 만들었습니다. 샘플 앱의 소스는 GitHub에 있습니다.
앱을 실행하면 OK와 CANCEL버튼이 있고 OK를 누르면 아래처럼 Dialog가 뜹니다.
아래 코드는 앱에서 OK버튼을 누르고, Dialog가 뜨는 것을 기다린 후 다시 OK 버튼을 누르는 테스트 코드입니다. 만약 중간에 View가 없거나 실행되지 않는다면 Assert가 발생하여 테스트는 실패합니다.
@Test
fun testButtonDialog() {
// Launch and wait for the app
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
val context = InstrumentationRegistry.getTargetContext()
val intent = context.packageManager
.getLaunchIntentForPackage(context.packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
device.wait(
Until.hasObject(By.pkg(context.packageName).depth(0)),
5000
)
// Find and click OK button
val okButton: UiObject2 = device.findObject(
By.text("OK").clazz("android.widget.Button")
) // 1
if (okButton.isEnabled && okButton.isClickable) {
okButton.click()
} // 2
// Wait for the dialog
val dlgTitleSelector: BySelector = By.text("My Dialog") // 3
device.wait(Until.hasObject(dlgTitleSelector), 1000) // 4
val dlgTitle: UiObject2 = device.findObject(
dlgTitleSelector
) // 5
val dlgDescription: UiObject2 = device.findObject(
By.text("we are testing with UIAutomator")
) // 6
// Click OK button
val dlgButton: UiObject2 = device.findObject(
By.text("OK")
) // 7
dlgButton.click()
}
- 앱에서 'OK' 텍스트를 찾습니다.
- 1에서 얻은 UiObject2객체가 클릭 가능하다면 클릭합니다.
- 2로 인해 실행될 Dialog의 Title 제목으로 Selector를 만듭니다.
- 3의 Selector로 Dialog가 뜰 때까지 기다립니다.
- Dialog가 실행된 후, title에 대한 UiObject2를 찾습니다. 여기서 추가로 무엇인가를 하진 않습니다.
- Dialog가 실행된 후, description 대한 UiObject2를 찾습니다. 여기서 추가로 무엇인가를 하진 않습니다.
- Dialog의 OK버튼에 대한 UiObject2를 찾고, 클릭합니다. 그럼 Dialog는 종료되고 테스트도 종료됩니다.
이 과정에서 객체를 찾지 못하면 테스트는 실패처리됩니다.
Selector를 만들 때 text로 설정하면 코드를 구현하기 쉽습니다. 하지만 텍스트가 변경될 때 테스트 코드도 변경되어야 하기 때문에 변하지 않는 속성으로 Selector를 만드는 것이 좋습니다.
예를 들어, 다국어를 지원하는 앱이라면 텍스트는 언어에 따라서 달라질 수 있습니다. 또는 UI가 변경되어 텍스트도 함께 변경될 수 있습니다. View의 리소스 ID가 변경되지 않는다면 이것을 이용하는 것이 더 좋을 수 있습니다.
정리
UIAutomator2의 기초적인 사용 방법을 알아보았습니다. 이정도면 간단한 테스트 코드를 작성할 수 있습니다. 테스트 코드 작성 중 다른 기능이 필요하거나, 더 자세한 것을 알아야 한다면 UIAutomator Reference나 다른 튜토리얼을 보시고 학습하시면 됩니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력