지금까지 앱의 로직을 테스트하는 법에 대해 알아보았는데요, 이번 영상에서는 앱의 화면을 테스트하는 법에 대해 알아보겠습니다.

실제 앱을 실행하고 나타난 화면이 기대한 값을 표시하고 있는지, 또는 버튼을 클릭했을 때 정해진 동작이 수행되는지 등을 테스트 하는 것을 UI 테스트라고 합니다. 안드로이드에서는 Espresso라는 API를 통해 UI 테스트를 수행하게 됩니다.
Espresso의 API는 다음과 같은 4가지 컴포넌트로 구성되어 있습니다.
onView 혹은 onData를 제공합니다. 꼭 뷰와 연결되지 않아도 사용할 수 있는 메소드도 있습니다.Espresso를 이용한 테스트 코드는 다음과 같습니다. onView로 테스트를 시작하고 withId로 원하는 View를 탐색한 뒤, perform으로 View에 실행할 동작을 정의하고, check로 View가 지정한 동작을 수행하게 되는 것이죠.
onView( // Espresso Entrypoint
withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher
.perform(click()) // click() is a ViewAction
.check(matches(isDisplayed())) // matches(isDisplayed()) is a ViewAssertion
Espresso는 빠른 테스트 속도가 UI의 비동기적 동작에 방해받지 않도록 다음과 같은 라이프사이클을 가지고 있습니다. 엔트리포인트를 만들고 비동기 작업이 모두 사라질 때까지 기다린 뒤에 뷰에 대한 작업을 수행하게 되는 것이죠.
Espresso에서 사용할 수 있는 여러가지 메소드들이 있는데요, 그것들을 정리한 Cheat Sheet는 다음과 같습니다.

그러면 Espresso를 사용해서 우리가 만든 BookSearchApp의 UI를 테스트 해 보도록 하겠습니다.
Espresso를 사용하기 위해서 espresso-core 디펜던시를 추가해야 하는데요, 이 디펜던시는 안드로이드 스튜디오에서 프로젝트를 생성할 경우 기본으로 추가됩니다. 그리고 Espresso의 적용 대상을 확장해주는 espresso-contrib도 추가해 줍니다.
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.4.0") // for recyclerview test
UI가 전환될 때 표시되는 애니메이션에 의한 딜레이때문에 테스트가 실행되지 못하는 경우가 있기 때문에 애니메이션을 꺼 줍니다. 설정 > 개발자 옵션 > 창 애니메이션 배율, 전환 애니메이션 비율, Animator 길이 배율을 모두 OFF로 하거나, 프로젝트 레벨의 build.gradle에 다음 코드를 추가합니다.
android {
testOptions {
animationsDisabled = true
}
}
그럼 UI 테스트를 만들어보도록 하겠습니다. 여기서는 SearchFragment에서 FavoriteFragment로 이어지는 일련의 동작을 다음과 같이 분해하여 테스트 해 보도록 하겠습니다.
// 1. SearchFragment
// 1-1) 리사이클러뷰 대신 `"No Result"`가 출력되는지 확인
// 1-2) 검색어로 `"android"`를 입력
// 1-3) 리사이클러뷰 표시를 확인
// 1-4) 첫번째 반환값을 클릭
// 1-5) BookFragment 결과를 저장
// 1-6) 이전 화면으로 돌아감
// 1-7) SnackBar가 사라질 때까지 대기
// 2. FavoriteFragment
// 2-1) FavoriteFragment로 이동
// 2-2) 리사이클러뷰 표시를 확인
// 2-3) 첫번째 아이템을 슬라이드하여 삭제
그런데 이대로 테스트를 만들면 android를 검색한 뒤 결과가 표시되기도 전에 리사이클러뷰 아이템을 클릭하기 때문에 테스트가 실패하게 됩니다. 그래서 네트워크 동작까지 완료될 때까지 앱에 대기 시간을 부여할 필요가 있습니다.
구글에서는 이러한 비동기 작업을 수행할 때 앱을 대기하게 하는 목적으로 Espresso idling resources API를 제공하고 있습니다. Life Of An Espresso Test 그림에서 보여드린 것 처럼, Espresso는 View를 찾기 전 Synchronized를 수행하고 상태가 Idle이어야 View를 찾는 단계로 넘어가게 되는데 Idling resources를 사용하면 Synchronize 시간을 지연시켜주게 됩니다.
다만 이 API의 문제는 테스트를 위해 프로덕션 코드까지 수정해야 하는 불편한 사용방식을 가지고 있다는 점입니다. 그렇다고 Thread.sleep을 써서 강제로 테스트를 지연시키는 것은 구글에서 금지하는 행위이기 때문에, 여기서는 Espresso의 대기를 위해 단순한 waitFor 메소드를 만들어 사용하도록 하겠습니다. Hamcrest 디펜던시를 추가하고 다음과 같이 메소드를 만들어 줍니다.
dependencies {
androidTestImplementation("org.hamcrest:hamcrest:2.2")
}
private fun waitFor(delay: Long): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = isRoot()
override fun getDescription(): String = "wait for $delay milliseconds"
override fun perform(uiController: UiController, view: View?) {
uiController.loopMainThreadForAtLeast(delay)
}
}
}
동작중 대기시간을 고려하여 Espresso의 테스트 코드를 작성하면 다음과 같습니다. 테스트 메소드는 기존의 MainActivityTest 클래스 안에 추가하였고, 시간이 오래 걸리는 테스트이므로 @LargeTest로 분류하였습니다. 또 앞선 강의에서 앱의 런너를 Hilt용으로 변경하였으므로 그에 따라 런너를 교체하여 Hilt의 엔트리포인트로 만들어주고 룰을 추가한 뒤 테스트를 실행하면 모든 동작이 정상적으로 수행되는 것을 확인할 수 있습니다.
@Test
@LargeTest
fun from_SearchFragment_to_FavoriteFragment_Ui_Operation() {
// 1. SearchFragment
// 1-1) 리사이클러뷰 대신 `"No Result"`가 출력되는지 확인
onView(withId(R.id.tv_emptylist))
.check(matches(withText("No result")))
// 1-2) 검색어로 `"android"`를 입력
onView(withId(R.id.et_search))
.perform(typeText("android"))
onView(isRoot()).perform(waitFor(3000))
// 1-3) 리사이클러뷰 표시를 확인
onView(withId(R.id.rv_search_result))
.check(matches(isDisplayed()))
// 1-4) 첫번째 반환값을 클릭
onView(withId(R.id.rv_search_result))
.perform(actionOnItemAtPosition<BookSearchViewHolder>(0, click()))
onView(isRoot()).perform(waitFor(1000))
// 1-5) BookFragment 결과를 저장
onView(withId(R.id.fab_favorite))
.perform(click())
// 1-6) 이전 화면으로 돌아감
pressBack()
// 1-7) SnackBar가 사라질 때까지 대기
onView(isRoot()).perform(waitFor(3000))
onView(withId(R.id.rv_search_result))
.check(matches(isDisplayed()))
// 2. FavoriteFragment
// 2-1) FavoriteFragment로 이동
onView(withId(R.id.fragment_favorite))
.perform(click())
// 2-2) 리사이클러뷰 표시를 확인
onView(withId(R.id.rv_favorite_books))
.check(matches(isDisplayed()))
// 2-3) 첫번째 아이템을 슬라이드하여 삭제
onView(withId(R.id.rv_favorite_books)).perform(
actionOnItemAtPosition<BookSearchViewHolder>(0, swipeLeft())
)
}
-@RunWith(AndroidJUnit4::class)
+@HiltAndroidTest
@SmallTest
class MainActivityTest {
+ @get:Rule
+ var hiltRule = HiltAndroidRule(this)
...
}
이렇게 해서 Espresso로 안드로이드 앱을 테스트 하는 방법에 대해 알아보았습니다.