SlideShare a Scribd company logo
1 of 28
Download to read offline
Android UI Test
Espresso & Kakao
UI Test
• 실제 디바이스 및 에뮬레이터에서 실행되는 테스트
• src/androidTest/java 경로에 작성된 테스트 파일을 통해 테스트 진행
• Unit Test 에 비해 속도가 느림
• 실제 동작에 대해 테스트를 진행하므로, 테스트 결과 신뢰성이 높음
• 실제 API 응답 데이터를 사용하여 테스트 가능 (IdlingResource 등 필요)
• 실제 API 응답 데이터를 사용하지 않을 경우 Mocking Data 가 필요 (mock flavor 및 Dagger 를 통해 구현 가능)
→ 해당 자료에서는 mock flavor 에 구현한 MockRepository 를 사용
• 주로 Espresso 사용
Settings - build.gradle
androidTestImplementation 'com.agoda.kakao:kakao:2.2.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1‘
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
app/build.gradle
Settings - flavor
……
flavorDimensions "applicationId"
productFlavors {
mock {
applicationIdSuffix ".mock"
}
prod {
applicationIdSuffix ".prod"
}
}
sourceSets {
mock {
java.srcDirs = ['src/mock/java']
}
prod {
java.srcDirs = ['src/prod/java']
}
}
……
app/build.gradle
Settings - flavor
……
flavorDimensions "applicationId"
productFlavors {
mock {
applicationIdSuffix ".mock"
}
prod {
applicationIdSuffix ".prod"
}
}
sourceSets {
mock {
java.srcDirs = ['src/mock/java']
}
prod {
java.srcDirs = ['src/prod/java']
}
}
……
1. androidTest
• UI 테스트 관련 코드 포함
• *Test / CustomMatcher / Screen 등
2. main
• 공통적으로 사용하는 코드 포함
3. mock
• UI 테스트 시 사용할 Mocking 코드 포함
• Repository 및 RepositoryInjection
4. prod
• 배포 등 실제 동작 시 사용할 코드 포함
• Repository 및 RepositoryInjection
Settings - flavor
……
flavorDimensions "applicationId"
productFlavors {
mock {
applicationIdSuffix ".mock"
}
prod {
applicationIdSuffix ".prod"
}
}
sourceSets {
mock {
java.srcDirs = ['src/mock/java']
}
prod {
java.srcDirs = ['src/prod/java']
}
}
……
UI Test 시
• [ Build Variants → mockDebug ] 선택하여 작성된 더미 데이터를 사용하도록 설정
※ prodDebug 선택 후 UI Test 시
• 실제 API 호출하여 테스트 진행
• API 호출 후 응답이 돌아올 때까지 대기하지 않고 테스트 진행 → FAIL
• IdlingResource 를 이용하여 API 응답이 돌아올 때까지 대기 가능
→ 서버 상태가 테스트코드 결과에 영향
→ 기존 코드 수정 발생
Settings - 개발자 옵션
애니메이션 관련 설정
• [ 설정 → 개발자 옵션 → 그림 ] 항목에서 각 애니메이션 옵션 비활성화
• Activity, Dialog 실행 등의 애니메이션을 비활성화
• UI Test 가 실행될 기기 또는 에뮬레이터 설정 필요
Simple MVP Example
Simple MVP Example
• GitHub 사용자명을 입력 받아 해당 사용자의 Repository 리스트 표시
• 각 아이템에 Repository 명과 Star 수 표시
• 각 아이템 클릭 시 Repository 로 이동 (ACTION_VIEW)
• 하단 버튼 클릭 시 Toast 표시
• https://github.com/MDLicht/SimpleMVPExample
Simple MVP Example - Test Code
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Rule
@JvmField
val rule = IntentsTestRule(MainActivity::class.java)
val screen = MainScreen()
……
}
ActivityTestRule / IntentsTestRule
• UI Test 를 실행하려는 화면 정의
• Intent 검증을 위한 intended, intending 사용 시 IntentsTestRule 사용
Screen
• agoda/Kakao Library 에 포함
• KView, KEditText, KRecyclerView 등 테스트용 View 정의
• 각 KView 에는 hasAnyText(), hasNoText() 등 Custom Assertion,
Custom Matcher 포함
• Screen 객체를 직접 생성 후 사용 또는 onScreen<SomeScreen> 을 사용할 수 있음
Simple MVP Example - Screen
Screen
• Espresso.onView() 로 매번 테스트할 View 를 찾는 코드 생략
• withId(), withText(), withTag() 등을 이용하여 View 를 설정
• RecyclerView 의 경우 itemType 을 추가적으로 설정
• 설정된 각 KView 에는 Espresso 에 미포함된 자주 사용하는 Custom Matcher 등이
포함
class MainScreen : Screen<MainScreen>() {
val search = KEditText { withId(R.id.search) }
val result = KRecyclerView({ withId(R.id.result) }
, itemTypeBuilder = { itemType(::Item) })
val emptyText = KTextView { withId(R.id.empty) }
val testButton = KButton { withId(R.id.testButton) }
}
class Item(parent: Matcher<View>)
: KRecyclerItem<Item>(parent) {
val title = KTextView(parent) { withId(R.id.title) }
val star = KTextView(parent) { withId(R.id.star) }
}
Test 01. Type Text
@Test
fun 검색어_입력() {
screen {
search {
typeText("mdlicht")
hasText("mdlicht")
}
}
}
1. Screen 내 search (KEditText) 에 문자열을 입력
2. Screen 내 search 에 해당 문자열이 입력되어 있는지 검사
• typeText() 는 EditText.setText() 처럼 지정된 문자열을 한 번에 입력하지 않고, 실제
입력처럼 한 글자씩 입력
• Kakao 에 구현된 clearText() 는 replaceText(“”) 를 실행
테스트 시 주의사항 01. typeText(), click() 등 Action
Action 을 실행할 View 가 스크롤 하단에 있는 경우
• Screen.testButton.click() 시 Test Fail 발생
→ click() 할 View 가 현재 화면에 보이지 않아 Action 의 대상을 찾지 못함
→ 스크롤링이 가능한 화면일 경우 scrollTo() 선행 필요
→ typeText() 등 다른 Action 의 경우에도 동일
Example)
screen {
testButton {
scrollTo()
click()
}
}
testButton
Test 02. Type Text / Search Result
1. Screen 내 search (KEditText) 에 문자열을 입력
2. Keyboard 의 IME Search 버튼 클릭
3. Screen 내 result (RecyclerView) 의 첫 번째 아이템의
title 및 star 에 표시된 내용 검사
4. result 의 마지막 아이템의 title 및 star 에 표시된 내용 검사
5. result 의 아이템 개수 검사
6. Screen 내 emptyText (TextView) 가 화면에 미표시
상태인지 검사
• RecyclerView 의 첫 번째, 마지막 아이템이 아닐 경우에는 childAt(position) 사용
@Test
fun 검색어_입력_검색버튼_클릭_검색결과_있음(){
screen {
search {
typeText("mdlicht")
pressImeAction()
}
result {
firstChild<Item> {
title {
hasText("sample1")
}
star {
hasText("100")
}
}
lastChild<Item> {
title {
hasText("sample4")
}
star {
hasText("120")
}
}
assertEquals(4, getSize())
}
emptyText {
isNotDisplayed()
}
}
}
테스트 시 주의사항 02. isDisplayed(), isVisible(), isNotDisplayed()
View 가 현재 화면에 표시되고 있는지 검사하는 경우
• KView.isDisplayed() 사용 가능 / Espresso 의 경우 onView(withId(R.id.some_id)).check(matches(isDisplayed()))
• 스크롤 등의 이유로 현재 화면에 보이지 않는 View 를 isDisplayed() 로 검사 시 Test Fail 발생
→ isDisplayed() : Visibility 값이 VISIBLE 인지 여부 + View 가 화면에 보이고 있는지 여부
→ isVisible() : Visibility 값이 VISIBLE 인지 여부
• 단순히 View 의 Visibility 설정값을 검사할 경우 isVisible() 사용
Test 03. Type Text / Empty Result
1. Screen 내 search (KEditText) 에 문자열을 입력
2. Keyboard 의 IME Search 버튼 클릭
3. Screen 내 emptyText (KTextView) 가 표시되는지 검사
(Visibility 값이 VISIBLE 인지 여부)
4. emptyText 에 지정한 문자열이 표시되는지 검사
5. Screen 내 result (RecyclerView) 가 미표시되는지 검사
• 현재 설정으로는 검색 성공/실패 경우의 더미 데이터를 나누지 못해
@Ignore Annotation 을 붙여, 테스트 미실행
@Ignore
@Test
fun 검색어_입력_검색버튼_클릭_검색결과_없음() {
screen {
search {
typeText("mdlicht")
pressImeAction()
}
emptyText {
isVisible()
hasText(R.string.empty_text)
}
result {
isNotDisplayed()
}
}
}
Test 04. Type Text / Search Result / Click Item /Intended
1. Screen 내 search (KEditText) 에 문자열을 입력
2. Keyboard 의 IME Search 버튼 클릭
3. Screen 내 result (KRecyclerView) 의 첫 번째 아이템 클릭
4. 실행된 Activity 의 Intent 설정을 검사
• intended : 테스트 과정 중 실행된 Activity 의 Intent 정보를 검사
(Action, Flag, Extra 등)
@Test
fun 검색결과_첫번쨰_아이템_클릭() {
screen {
search {
typeText("mdlicht")
pressImeAction()
}
result {
firstChild<Item> {
click()
}
}
intended(hasAction(Intent.ACTION_VIEW))
intended(hasData(Uri.parse("http://www.google.com")))
intended(hasExtra("test", 100))
intended(hasExtra("test22", "sample"))
}
}
Test 05. Button Click / Intending / onActivityResult
1. onActivityResult() 로 전달될 ResultData 를 설정
2. TestActivity 컴포넌트명을 가진 Intent 실행 시 1)에서 생성한
ResultData 를 반환하도록 설정
3. Screen 내 testButton (KButton) 클릭
4. Toast 에 1) 에서 설정한 문자열이 표시되는지 검사
• Toast, Dialog 표시 테스트 시 withId(), withText() 등으로 View 를 찾아
표시 여부 검사
@Test
fun 하단버튼_클릭(){
screen {
val resultData = Intent().apply {
putExtra("key", "THIS IS SIMPLE TEST")
}
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)
intending(
hasComponent(
ComponentName(rule.activity.application, TestActivity::class.java)
)
).respondWith(result)
testButton {
click()
}
onView(withText("THIS IS SIMPLE TEST"))
.inRoot(withDecorView(not(`is`(rule.activity.window.decorView))))
.check(matches(isDisplayed()))
}
}
Custom Matcher - TextColor
object CustomMatcher {
……
private class TextColorMatcher(val textColor: Int)
: BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("input text color : $textColor")
}
override fun matchesSafely(item: TextView?): Boolean {
val actualTextColor = item?.currentTextColor
return actualTextColor != null && actualTextColor == textColor
}
}
……
……
private class TextColorMatcher(val textColor: Int)
: BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("input text color : $textColor")
}
override fun matchesSafely(item: TextView?): Boolean {
val actualTextColor = item?.currentTextColor
return actualTextColor != null && actualTextColor == textColor
}
}
……
}
Custom Action - TextColor
object CustomMatcher {
class CheckViewAction(val checked: Boolean): ViewAction {
override fun getDescription(): String {
return "Set checked state"
}
override fun getConstraints(): Matcher<View> {
return object : BaseMatcher<View>() {
override fun describeTo(description: Description?) {
// do nothing
}
override fun matches(item: Any?): Boolean {
return isA(Checkable::class.java).matches(item)
}
}
}
override fun perform(uiController: UiController?, view: View?) {
(view as? Checkable)?.let {
it.isChecked = checked
}
}
}
}
UI Test with Asynchronous operation
• 실제 동작에서 사용하는 API 통신 등이 있을 경우 UI Test 는 API 응답과 관계없이 테스트 진행 → 실패
• API 응답을 받을 때까지 대기하는 방법
1. Thread.sleep()
2. CountDownLatch
3. Retry wrappers
→ 응답을 받은 것을 완벽하게 보장하기에는 제한적
→ IdlingResource
IdlingResource
• 활성화된 작업이 있는지 여부를 관리
• CountingIdlingResource 의 경우 현재 활성화된 작업의 갯수로 관리
• 사용 사례
• 서버 또는 로컬 데이터로부터 데이터를 불러오는 경우
• 비트맵 관련 등과 같은 복잡한 기능 수행
• 구현 예시
• CountingIdlingResource
• UriIdlingResource
• IdlingThreadPoolExecutor
• IdlingScheduledThreadPoolExecutor
IdlingResource Example 01 - Manager
class IdlingResourceManager private constructor(){
val countingIdlingResource =
CountingIdlingResource("ResourceName")
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
countingIdlingResource.decrement()
}
fun getIdlingResource(): IdlingResource {
return countingIdlingResource
}
companion object {
private var instance: IdlingResourceManager? = null
@JvmStatic
fun getInstance(): IdlingResourceManager {
if (instance == null) {
instance = IdlingResourceManager()
}
return instance!!
}
}
}
mock/java/IdlingResourceManager
IdlingResource Example 02 - Register and Unregister on @Before / @After
@Before
fun onBefore() {
IdlingRegistry.getInstance().register(IdlingResourceManager.getInstance().getIdlingResource())
}
@After
fun onAfter() {
IdlingRegistry.getInstance().unregister(IdlingResourceManager.getInstance().getIdlingResource())
}
IdlingResource Example 03 - Counting on repository
override fun doSomething() {
IdlingResourceManager.getInstance().increment()
// Do something… Access API, Database and ETC
IdlingResourceManager.getInstance().decrement()
}
IdlingResource Example 04 - Test Code
@Test
fun test_doSomething() {
screen {
button {
click() // call doSomething()
}
result {
isDisplayed() // await response
}
}
}
Command
에뮬레이터 실행
• emulator -avd ${EMULATOR_NAME}
• ex) emulator -avd Google_Pixel_3a_API_26
UI Test 실행
• ./gradlew connected${Flavor_BuildType}AndroidTest
• ex) ./gradlew connectedMockDebugAndroidTest
문제 및 과제
• 각 테스트 함수별 Mocking 설정 방법 미해결
• Mocking Data 를 테스트 함수 시작 시 설정, 주입
→ static 변수와 차이가 없음
→ static 변수일 경우, 다음 테스트 함수 실행 시 값 초기화 과정 필요
→ 데이터 주입을 보장하기 위해 매 테스트 함수 실행 시 rule.launchActivity(Intent()) 를 통해 화면 재실행 필요
• rule.activity.presenter.repository 를 통해 Repository 에 직접 접근하여 데이터 관리
→ 테스트코드 작성을 위해 presenter 의 접근제한자가 변경되어야함 (private → public)
→ 데이터 주입을 보장하기 위해 매 테스트 함수 실행 시 화면 재실행 필요
• Jenkins 를 통한 테스트
• Jenkins 내 에뮬레이터 사용 가능하도록 설정 필요
• 여러 UI Test 실행 시 다수의 에뮬레이터 실행 필요한지 조사 필요
• 1개의 에뮬레이터 상에서 다수의 UI Test 가 동시에 실행될 수 있는지 조사 필요
• 에뮬레이터 실행/종료 시점 정의 필요
Reference
Kakao
• https://github.com/agoda-com/Kakao
• https://github.com/agoda-com/Kakao/tree/master/sample/src/androidTest/kotlin/com/agoda/sample
Gradle
• https://developer.android.com/studio/test/command-line
IdlingResource
• https://developer.android.com/reference/android/support/test/espresso/IdlingResource
• https://developer.android.com/training/testing/espresso/idling-resource
• https://android.jlelse.eu/integrate-espresso-idling-resources-in-your-app-to-build-flexible-ui-tests-c779e24f5057

More Related Content

Similar to Android UI Test (Espresso/Kakao)

Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Testbeom kyun choi
 
Droid knights android test @Droid Knights 2018
Droid knights android test @Droid Knights 2018Droid knights android test @Droid Knights 2018
Droid knights android test @Droid Knights 2018KyungHo Jung
 
자바 테스트 자동화
자바 테스트 자동화자바 테스트 자동화
자바 테스트 자동화Sungchul Park
 
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드SangIn Choung
 
10장 결과 검증
10장 결과 검증10장 결과 검증
10장 결과 검증dagri82
 
[2011 04 11]mock_object 소개
[2011 04 11]mock_object 소개[2011 04 11]mock_object 소개
[2011 04 11]mock_object 소개Jong Pil Won
 
막하는 스터디 네 번째 만남 AngularJs (20151108)
막하는 스터디 네 번째 만남 AngularJs (20151108)막하는 스터디 네 번째 만남 AngularJs (20151108)
막하는 스터디 네 번째 만남 AngularJs (20151108)연웅 조
 
컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기우영 주
 
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기현철 조
 
안드로이드 개발자를 위한 스위프트
안드로이드 개발자를 위한 스위프트안드로이드 개발자를 위한 스위프트
안드로이드 개발자를 위한 스위프트병한 유
 
TDD.JUnit.조금더.알기
TDD.JUnit.조금더.알기TDD.JUnit.조금더.알기
TDD.JUnit.조금더.알기Wonchang Song
 
Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드
Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드 Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드
Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드 SangIn Choung
 
Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005Ryan Park
 
Android 기초강좌 애플리캐이션 구조
Android 기초강좌 애플리캐이션 구조Android 기초강좌 애플리캐이션 구조
Android 기초강좌 애플리캐이션 구조Sangon Lee
 
[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱NAVER D2
 
C++ 프로젝트에 단위 테스트 도입하기
C++ 프로젝트에 단위 테스트 도입하기C++ 프로젝트에 단위 테스트 도입하기
C++ 프로젝트에 단위 테스트 도입하기Heo Seungwook
 
Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기hanbeom Park
 

Similar to Android UI Test (Espresso/Kakao) (20)

Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
 
Droid knights android test @Droid Knights 2018
Droid knights android test @Droid Knights 2018Droid knights android test @Droid Knights 2018
Droid knights android test @Droid Knights 2018
 
자바 테스트 자동화
자바 테스트 자동화자바 테스트 자동화
자바 테스트 자동화
 
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
 
Coded ui가이드
Coded ui가이드Coded ui가이드
Coded ui가이드
 
Cygnus unit test
Cygnus unit testCygnus unit test
Cygnus unit test
 
10장 결과 검증
10장 결과 검증10장 결과 검증
10장 결과 검증
 
Swt J Face 2/3
Swt J Face 2/3Swt J Face 2/3
Swt J Face 2/3
 
[2011 04 11]mock_object 소개
[2011 04 11]mock_object 소개[2011 04 11]mock_object 소개
[2011 04 11]mock_object 소개
 
막하는 스터디 네 번째 만남 AngularJs (20151108)
막하는 스터디 네 번째 만남 AngularJs (20151108)막하는 스터디 네 번째 만남 AngularJs (20151108)
막하는 스터디 네 번째 만남 AngularJs (20151108)
 
컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기
 
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
[NDC17] Unreal.js - 자바스크립트로 쉽고 빠른 UE4 개발하기
 
안드로이드 개발자를 위한 스위프트
안드로이드 개발자를 위한 스위프트안드로이드 개발자를 위한 스위프트
안드로이드 개발자를 위한 스위프트
 
TDD.JUnit.조금더.알기
TDD.JUnit.조금더.알기TDD.JUnit.조금더.알기
TDD.JUnit.조금더.알기
 
Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드
Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드 Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드
Postman과 Newman을 이용한 RestAPI 테스트 자동화 가이드
 
Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005Working Effectively With Legacy Code - xp2005
Working Effectively With Legacy Code - xp2005
 
Android 기초강좌 애플리캐이션 구조
Android 기초강좌 애플리캐이션 구조Android 기초강좌 애플리캐이션 구조
Android 기초강좌 애플리캐이션 구조
 
[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱
 
C++ 프로젝트에 단위 테스트 도입하기
C++ 프로젝트에 단위 테스트 도입하기C++ 프로젝트에 단위 테스트 도입하기
C++ 프로젝트에 단위 테스트 도입하기
 
Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기Android Native Module 안정적으로 개발하기
Android Native Module 안정적으로 개발하기
 

Android UI Test (Espresso/Kakao)

  • 2. UI Test • 실제 디바이스 및 에뮬레이터에서 실행되는 테스트 • src/androidTest/java 경로에 작성된 테스트 파일을 통해 테스트 진행 • Unit Test 에 비해 속도가 느림 • 실제 동작에 대해 테스트를 진행하므로, 테스트 결과 신뢰성이 높음 • 실제 API 응답 데이터를 사용하여 테스트 가능 (IdlingResource 등 필요) • 실제 API 응답 데이터를 사용하지 않을 경우 Mocking Data 가 필요 (mock flavor 및 Dagger 를 통해 구현 가능) → 해당 자료에서는 mock flavor 에 구현한 MockRepository 를 사용 • 주로 Espresso 사용
  • 3. Settings - build.gradle androidTestImplementation 'com.agoda.kakao:kakao:2.2.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:core:1.1.0' androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1‘ implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' app/build.gradle
  • 4. Settings - flavor …… flavorDimensions "applicationId" productFlavors { mock { applicationIdSuffix ".mock" } prod { applicationIdSuffix ".prod" } } sourceSets { mock { java.srcDirs = ['src/mock/java'] } prod { java.srcDirs = ['src/prod/java'] } } …… app/build.gradle
  • 5. Settings - flavor …… flavorDimensions "applicationId" productFlavors { mock { applicationIdSuffix ".mock" } prod { applicationIdSuffix ".prod" } } sourceSets { mock { java.srcDirs = ['src/mock/java'] } prod { java.srcDirs = ['src/prod/java'] } } …… 1. androidTest • UI 테스트 관련 코드 포함 • *Test / CustomMatcher / Screen 등 2. main • 공통적으로 사용하는 코드 포함 3. mock • UI 테스트 시 사용할 Mocking 코드 포함 • Repository 및 RepositoryInjection 4. prod • 배포 등 실제 동작 시 사용할 코드 포함 • Repository 및 RepositoryInjection
  • 6. Settings - flavor …… flavorDimensions "applicationId" productFlavors { mock { applicationIdSuffix ".mock" } prod { applicationIdSuffix ".prod" } } sourceSets { mock { java.srcDirs = ['src/mock/java'] } prod { java.srcDirs = ['src/prod/java'] } } …… UI Test 시 • [ Build Variants → mockDebug ] 선택하여 작성된 더미 데이터를 사용하도록 설정 ※ prodDebug 선택 후 UI Test 시 • 실제 API 호출하여 테스트 진행 • API 호출 후 응답이 돌아올 때까지 대기하지 않고 테스트 진행 → FAIL • IdlingResource 를 이용하여 API 응답이 돌아올 때까지 대기 가능 → 서버 상태가 테스트코드 결과에 영향 → 기존 코드 수정 발생
  • 7. Settings - 개발자 옵션 애니메이션 관련 설정 • [ 설정 → 개발자 옵션 → 그림 ] 항목에서 각 애니메이션 옵션 비활성화 • Activity, Dialog 실행 등의 애니메이션을 비활성화 • UI Test 가 실행될 기기 또는 에뮬레이터 설정 필요
  • 8. Simple MVP Example Simple MVP Example • GitHub 사용자명을 입력 받아 해당 사용자의 Repository 리스트 표시 • 각 아이템에 Repository 명과 Star 수 표시 • 각 아이템 클릭 시 Repository 로 이동 (ACTION_VIEW) • 하단 버튼 클릭 시 Toast 표시 • https://github.com/MDLicht/SimpleMVPExample
  • 9. Simple MVP Example - Test Code @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Rule @JvmField val rule = IntentsTestRule(MainActivity::class.java) val screen = MainScreen() …… } ActivityTestRule / IntentsTestRule • UI Test 를 실행하려는 화면 정의 • Intent 검증을 위한 intended, intending 사용 시 IntentsTestRule 사용 Screen • agoda/Kakao Library 에 포함 • KView, KEditText, KRecyclerView 등 테스트용 View 정의 • 각 KView 에는 hasAnyText(), hasNoText() 등 Custom Assertion, Custom Matcher 포함 • Screen 객체를 직접 생성 후 사용 또는 onScreen<SomeScreen> 을 사용할 수 있음
  • 10. Simple MVP Example - Screen Screen • Espresso.onView() 로 매번 테스트할 View 를 찾는 코드 생략 • withId(), withText(), withTag() 등을 이용하여 View 를 설정 • RecyclerView 의 경우 itemType 을 추가적으로 설정 • 설정된 각 KView 에는 Espresso 에 미포함된 자주 사용하는 Custom Matcher 등이 포함 class MainScreen : Screen<MainScreen>() { val search = KEditText { withId(R.id.search) } val result = KRecyclerView({ withId(R.id.result) } , itemTypeBuilder = { itemType(::Item) }) val emptyText = KTextView { withId(R.id.empty) } val testButton = KButton { withId(R.id.testButton) } } class Item(parent: Matcher<View>) : KRecyclerItem<Item>(parent) { val title = KTextView(parent) { withId(R.id.title) } val star = KTextView(parent) { withId(R.id.star) } }
  • 11. Test 01. Type Text @Test fun 검색어_입력() { screen { search { typeText("mdlicht") hasText("mdlicht") } } } 1. Screen 내 search (KEditText) 에 문자열을 입력 2. Screen 내 search 에 해당 문자열이 입력되어 있는지 검사 • typeText() 는 EditText.setText() 처럼 지정된 문자열을 한 번에 입력하지 않고, 실제 입력처럼 한 글자씩 입력 • Kakao 에 구현된 clearText() 는 replaceText(“”) 를 실행
  • 12. 테스트 시 주의사항 01. typeText(), click() 등 Action Action 을 실행할 View 가 스크롤 하단에 있는 경우 • Screen.testButton.click() 시 Test Fail 발생 → click() 할 View 가 현재 화면에 보이지 않아 Action 의 대상을 찾지 못함 → 스크롤링이 가능한 화면일 경우 scrollTo() 선행 필요 → typeText() 등 다른 Action 의 경우에도 동일 Example) screen { testButton { scrollTo() click() } } testButton
  • 13. Test 02. Type Text / Search Result 1. Screen 내 search (KEditText) 에 문자열을 입력 2. Keyboard 의 IME Search 버튼 클릭 3. Screen 내 result (RecyclerView) 의 첫 번째 아이템의 title 및 star 에 표시된 내용 검사 4. result 의 마지막 아이템의 title 및 star 에 표시된 내용 검사 5. result 의 아이템 개수 검사 6. Screen 내 emptyText (TextView) 가 화면에 미표시 상태인지 검사 • RecyclerView 의 첫 번째, 마지막 아이템이 아닐 경우에는 childAt(position) 사용 @Test fun 검색어_입력_검색버튼_클릭_검색결과_있음(){ screen { search { typeText("mdlicht") pressImeAction() } result { firstChild<Item> { title { hasText("sample1") } star { hasText("100") } } lastChild<Item> { title { hasText("sample4") } star { hasText("120") } } assertEquals(4, getSize()) } emptyText { isNotDisplayed() } } }
  • 14. 테스트 시 주의사항 02. isDisplayed(), isVisible(), isNotDisplayed() View 가 현재 화면에 표시되고 있는지 검사하는 경우 • KView.isDisplayed() 사용 가능 / Espresso 의 경우 onView(withId(R.id.some_id)).check(matches(isDisplayed())) • 스크롤 등의 이유로 현재 화면에 보이지 않는 View 를 isDisplayed() 로 검사 시 Test Fail 발생 → isDisplayed() : Visibility 값이 VISIBLE 인지 여부 + View 가 화면에 보이고 있는지 여부 → isVisible() : Visibility 값이 VISIBLE 인지 여부 • 단순히 View 의 Visibility 설정값을 검사할 경우 isVisible() 사용
  • 15. Test 03. Type Text / Empty Result 1. Screen 내 search (KEditText) 에 문자열을 입력 2. Keyboard 의 IME Search 버튼 클릭 3. Screen 내 emptyText (KTextView) 가 표시되는지 검사 (Visibility 값이 VISIBLE 인지 여부) 4. emptyText 에 지정한 문자열이 표시되는지 검사 5. Screen 내 result (RecyclerView) 가 미표시되는지 검사 • 현재 설정으로는 검색 성공/실패 경우의 더미 데이터를 나누지 못해 @Ignore Annotation 을 붙여, 테스트 미실행 @Ignore @Test fun 검색어_입력_검색버튼_클릭_검색결과_없음() { screen { search { typeText("mdlicht") pressImeAction() } emptyText { isVisible() hasText(R.string.empty_text) } result { isNotDisplayed() } } }
  • 16. Test 04. Type Text / Search Result / Click Item /Intended 1. Screen 내 search (KEditText) 에 문자열을 입력 2. Keyboard 의 IME Search 버튼 클릭 3. Screen 내 result (KRecyclerView) 의 첫 번째 아이템 클릭 4. 실행된 Activity 의 Intent 설정을 검사 • intended : 테스트 과정 중 실행된 Activity 의 Intent 정보를 검사 (Action, Flag, Extra 등) @Test fun 검색결과_첫번쨰_아이템_클릭() { screen { search { typeText("mdlicht") pressImeAction() } result { firstChild<Item> { click() } } intended(hasAction(Intent.ACTION_VIEW)) intended(hasData(Uri.parse("http://www.google.com"))) intended(hasExtra("test", 100)) intended(hasExtra("test22", "sample")) } }
  • 17. Test 05. Button Click / Intending / onActivityResult 1. onActivityResult() 로 전달될 ResultData 를 설정 2. TestActivity 컴포넌트명을 가진 Intent 실행 시 1)에서 생성한 ResultData 를 반환하도록 설정 3. Screen 내 testButton (KButton) 클릭 4. Toast 에 1) 에서 설정한 문자열이 표시되는지 검사 • Toast, Dialog 표시 테스트 시 withId(), withText() 등으로 View 를 찾아 표시 여부 검사 @Test fun 하단버튼_클릭(){ screen { val resultData = Intent().apply { putExtra("key", "THIS IS SIMPLE TEST") } val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) intending( hasComponent( ComponentName(rule.activity.application, TestActivity::class.java) ) ).respondWith(result) testButton { click() } onView(withText("THIS IS SIMPLE TEST")) .inRoot(withDecorView(not(`is`(rule.activity.window.decorView)))) .check(matches(isDisplayed())) } }
  • 18. Custom Matcher - TextColor object CustomMatcher { …… private class TextColorMatcher(val textColor: Int) : BoundedMatcher<View, TextView>(TextView::class.java) { override fun describeTo(description: Description?) { description?.appendText("input text color : $textColor") } override fun matchesSafely(item: TextView?): Boolean { val actualTextColor = item?.currentTextColor return actualTextColor != null && actualTextColor == textColor } } …… …… private class TextColorMatcher(val textColor: Int) : BoundedMatcher<View, TextView>(TextView::class.java) { override fun describeTo(description: Description?) { description?.appendText("input text color : $textColor") } override fun matchesSafely(item: TextView?): Boolean { val actualTextColor = item?.currentTextColor return actualTextColor != null && actualTextColor == textColor } } …… }
  • 19. Custom Action - TextColor object CustomMatcher { class CheckViewAction(val checked: Boolean): ViewAction { override fun getDescription(): String { return "Set checked state" } override fun getConstraints(): Matcher<View> { return object : BaseMatcher<View>() { override fun describeTo(description: Description?) { // do nothing } override fun matches(item: Any?): Boolean { return isA(Checkable::class.java).matches(item) } } } override fun perform(uiController: UiController?, view: View?) { (view as? Checkable)?.let { it.isChecked = checked } } } }
  • 20. UI Test with Asynchronous operation • 실제 동작에서 사용하는 API 통신 등이 있을 경우 UI Test 는 API 응답과 관계없이 테스트 진행 → 실패 • API 응답을 받을 때까지 대기하는 방법 1. Thread.sleep() 2. CountDownLatch 3. Retry wrappers → 응답을 받은 것을 완벽하게 보장하기에는 제한적 → IdlingResource
  • 21. IdlingResource • 활성화된 작업이 있는지 여부를 관리 • CountingIdlingResource 의 경우 현재 활성화된 작업의 갯수로 관리 • 사용 사례 • 서버 또는 로컬 데이터로부터 데이터를 불러오는 경우 • 비트맵 관련 등과 같은 복잡한 기능 수행 • 구현 예시 • CountingIdlingResource • UriIdlingResource • IdlingThreadPoolExecutor • IdlingScheduledThreadPoolExecutor
  • 22. IdlingResource Example 01 - Manager class IdlingResourceManager private constructor(){ val countingIdlingResource = CountingIdlingResource("ResourceName") fun increment() { countingIdlingResource.increment() } fun decrement() { countingIdlingResource.decrement() } fun getIdlingResource(): IdlingResource { return countingIdlingResource } companion object { private var instance: IdlingResourceManager? = null @JvmStatic fun getInstance(): IdlingResourceManager { if (instance == null) { instance = IdlingResourceManager() } return instance!! } } } mock/java/IdlingResourceManager
  • 23. IdlingResource Example 02 - Register and Unregister on @Before / @After @Before fun onBefore() { IdlingRegistry.getInstance().register(IdlingResourceManager.getInstance().getIdlingResource()) } @After fun onAfter() { IdlingRegistry.getInstance().unregister(IdlingResourceManager.getInstance().getIdlingResource()) }
  • 24. IdlingResource Example 03 - Counting on repository override fun doSomething() { IdlingResourceManager.getInstance().increment() // Do something… Access API, Database and ETC IdlingResourceManager.getInstance().decrement() }
  • 25. IdlingResource Example 04 - Test Code @Test fun test_doSomething() { screen { button { click() // call doSomething() } result { isDisplayed() // await response } } }
  • 26. Command 에뮬레이터 실행 • emulator -avd ${EMULATOR_NAME} • ex) emulator -avd Google_Pixel_3a_API_26 UI Test 실행 • ./gradlew connected${Flavor_BuildType}AndroidTest • ex) ./gradlew connectedMockDebugAndroidTest
  • 27. 문제 및 과제 • 각 테스트 함수별 Mocking 설정 방법 미해결 • Mocking Data 를 테스트 함수 시작 시 설정, 주입 → static 변수와 차이가 없음 → static 변수일 경우, 다음 테스트 함수 실행 시 값 초기화 과정 필요 → 데이터 주입을 보장하기 위해 매 테스트 함수 실행 시 rule.launchActivity(Intent()) 를 통해 화면 재실행 필요 • rule.activity.presenter.repository 를 통해 Repository 에 직접 접근하여 데이터 관리 → 테스트코드 작성을 위해 presenter 의 접근제한자가 변경되어야함 (private → public) → 데이터 주입을 보장하기 위해 매 테스트 함수 실행 시 화면 재실행 필요 • Jenkins 를 통한 테스트 • Jenkins 내 에뮬레이터 사용 가능하도록 설정 필요 • 여러 UI Test 실행 시 다수의 에뮬레이터 실행 필요한지 조사 필요 • 1개의 에뮬레이터 상에서 다수의 UI Test 가 동시에 실행될 수 있는지 조사 필요 • 에뮬레이터 실행/종료 시점 정의 필요
  • 28. Reference Kakao • https://github.com/agoda-com/Kakao • https://github.com/agoda-com/Kakao/tree/master/sample/src/androidTest/kotlin/com/agoda/sample Gradle • https://developer.android.com/studio/test/command-line IdlingResource • https://developer.android.com/reference/android/support/test/espresso/IdlingResource • https://developer.android.com/training/testing/espresso/idling-resource • https://android.jlelse.eu/integrate-espresso-idling-resources-in-your-app-to-build-flexible-ui-tests-c779e24f5057