ViewMatchers, ViewActions, ViewAssertions에 포함된 함수들과 어떤 식으로 사용해야 하는지를 설명하고,
주의할 점들을 알아보려 한다.
ViewMatchers
뷰의 상태와 Matcher 함수들을 모아 놓은 클래스이다.
ViewMatchers는 뷰를 찾기 위해서도 ( onView ) 쓰고, 뷰의 상태를 확인하기 ( check ) 위해서도 사용한다.
onView(withText("Welcome"))
.check(matches(allOf(isDisplayed(),withText("Welcome"))))
위의 코드에서 사용된 withText, isDisplayed 같은 것들이 ViewMatcher이다.
ViewMatchers안에는 몇십 개 이상의 함수가 있기에 모두 소개할 수 없고, 몇 가지만 소개하려 한다.
1. isDisplayed
onView(isDisplayed())
가장 기본적인 ViewMatcher로 화면에 보이는지 여부로 판별한다.
단순히 VISIBLE 확인이 아니라 화면의 보이는 영역에 그려졌는지를 본다.
2. withId
onView(withId(R.id.button))
가장 쉽게 사용할 수 있는 id를 이용하여 판별하는 ViewMatcher이다.
3. withText
onView(withText("Button"))
onView(withText(R.string.button))
onView(withText(object : CustomMatcher<String>("has multiple 't'"){
override fun matches(item: Any?): Boolean {
return (item as? String)?.let{
it.filter { it == 't' }.count() == 2
}?: false
}
}))
텍스트를 이용하여 대상을 찾는 방법이다. 기본적인 String이나, stringRes를 지원한다.
추가적으로 커스텀한 매처를 사용할 수 도 있다.
대부분의 ViewMatcher 들은 커스텀한 Matcher 인자로 받을 수 있도록 되어 있다.
(withId 역시 커스텀 매처가 가능하다 )
위의 예제의 커스텀 매처는 't'를 두 개 가지고 있는 뷰를 찾는 로직이다.
"Button" 은 't'를 두 개 가졌기에 통과될 수 있다.
withText 안에 커스텀 매처를 넣은 경우 matches
의 item
에는 화면에 존재하는 모든 뷰들의 text 값이 들어오게 되고,
이 값을 이용해 원하는 뷰를 찾도록 구현하면 된다.
( withText -> text , withId -> id , withContentDescription -> contentDescription 가 item으로 들어온다고 생각하면 된다. )
예제를 위한 것이지 실제로 onView에 저런 매처를 넣으면 뷰가 여러 개 선택될 확률이 높으니 주의해야 한다.
4. hasContentDescription, withContentDescription
onView(allOf(hasContentDescription(), withContentDescription("is TextView")))
contentDescription을 이용하는 방법이다.
hasContentDescription는 contentDescription 이 있는지 확인하고,
withContentDescription는 withText와 동일하게 contentDescription가 동일한 게 있는지 확인한다.
여러 개의 매처를 통해 뷰를 찾으려면 위와 같이 allOf
함수를 사용하면 된다.
5. withParent, withChild, hasSibling, hasDescendant
withParent(withText("Welcome")) // 부모에게서
withChild(withText("Welcome")) // 자식들에게서
hasSibling(withText("Welcome")) // 같은 계층의 뷰들
hasDescendant(withText("Welcome")) // 하위 계층의 모든 뷰들
onView(allOf(withId(R.id.linearLayout), hasDescendant(withText("Hello World!"))))
.check(matches(isDisplayed()))
위의 함수들은 관계를 통해 탐색 대상을 지정하는 함수들이다.
위의 예제는 linearLayout을 찾고, 그 안에서 "Hello World!" 란 텍스트를 가진 뷰를 찾는 것이다.
독립적으로는 사용할 수 없고, 뷰가 주어졌을 때 해당 뷰와 관계를 바탕으로 검색을 한다.
주의할 점은 withChild
는 말 그대로 자기 자식만 챙기고 손자까지는 커버 치지 않는다.hasDescendant
는 자식에 손자에 증손자까지 다 커버 친다.
두 개를 혼동해서 사용하면 안 된다.
6. isDescendantOfA
onView(withId(R.id.textView))
.check(matches(isDescendantOfA(withId(R.id.linearLayout))))
isDescendantOfA
는 자신의 조상을 찾는 함수이다.hasDescendant
와 마찬가지로 부모에 조부모에 증조부모까지 다 찾아낸다.
7. isAssignableFrom
onView(Matchers.allOf(isAssignableFrom(TextView::class.java), withText("Hello World!")))
.check(matches(isDisplayed()))
클래스 타입을 통해 찾는 함수도 있다.isAssignableFrom
에 인자로 타입을 넣으면 된다. 부모 클래스로도 찾을 수 있다.
8. 그 외..
위의 함수 외에도isClickable
, isEnabled
, isSelected
, withClassName
등이 존재한다.
ViewActions
뷰에게 동작을 시키기 위한 함수들이 모여있는 클래스이다.
1. click, longClick, doubleClick
onView(withId(R.id.button))
.perform(click(), longClick())
.perform(doubleClick())
기본적인 클릭 동작이다.
위의 코드는 클릭 -> 롱 클릭 -> 더블 클릭이 순차적으로 일어난다.
perform
에 여러 개의 ViewAction을 넣을 수 도 있다. 한 번에 다 넣으나 따로 넣으나 차이는 없다.
주의할 것은 해당 ViewAction 은 뷰에 직접 클릭 이벤트를 보내는 것이 아니고, 해당 좌표에 클릭을 하는 방식이다.
그래서 해당 좌표 위에 다른 뷰가 올라와 있다면 전혀 다른 상황이 일어날 수 있으니, 주의해야 한다.
( 구글이 애니메이션을 끄라는 이유 중 하나이다.
ex) 클릭이 일어나는 순간 애니메이션이 버튼 위를 지나가서 클릭을 대신 먹고 도망가버리는 상황이 연출될 수 있다. )
2. clearText, typeText, typeTextIntoFocusedView
onView(withId(R.id.editText))
.perform(clearText())
.perform(typeText("Hello"))
.perform(typeTextIntoFocusedView("World"))
텍스트를 타이핑하기 위한 함수이다.
EditText와 같이 타이핑이 가능한 뷰에만 사용해야 한다.
clearText
는 텍스트를 지우는 동작을 하고,typeText
, typeTextIntoFocusedView
은 타이핑하는 동작을 한다.
그럼 타이핑은 왜 두 개인가?typeText
의 경우 포커싱 잡고, 타이핑을 하는 두 가지 동작을 진행하는 반면,typeTextIntoFocusedView
는 이미 포커싱이 잡힌 뷰에서 동작하고, 대상 뷰에게 포커싱이 없다면 에러를 발생시킨다.
그래서 왜?
위에 클릭에서 나왔듯이 에스프레소는 좌표를 기반으로 동작을 한다. 포커싱 역시 마찬가지이며 이때 좌표는 뷰의 중앙이다.
그럼 typeText
를 연달아 쓰면 어떻게 되느냐
이렇게 된다.
"Hello" 치고 그 중앙인 뷰의 중앙인 'e', 'l' 사이에 포커싱을 다시 잡고 "World"를 쳐버린다.
그러니, 여러 번 타이핑을 하고 싶으면 위의 예제 코드처럼 작성하면 된다.
3. replaceText
onView(withId(R.id.editText))
.perform(replaceText("Hello World")) //Hello World
.perform(typeTextIntoFocusedView("!!")) //!!Hello World
replaceText
는 텍스트를 대체하는 함수이다.
역시나 타이핑이 가능한 뷰에만 사용해야 한다.
해당 함수를 실행하면 "Hello World"로 텍스트가 대체된다.
주의할 점은 대체된 후 포커싱은 글자 맨 앞으로 이동하며,
그 상태로 typeTextIntoFocusedView
를 실행하면 맨 앞에 글자를 쓰기 시작한다. ( 최악이다. )
( 에스프레소에서 해결할 방법은 못 찾았고, 뷰를 들고 와서 포커싱을 "직접" 수정해주는 거 말고는 없는 것 같다... )
4. scrollTo
onView(withId(R.id.editText))
.perform(scrollTo(), click())
만약 화면에 없고, "ScrollView" 안에 있는 부라면 해당 뷰가 보일 때까지 스크롤을 해주어야 한다.
이때, scrollTo
를 사용해주면 된다.
5. swipeUp, swipeDown, swipeRight, swipeLeft
perform(swipeUp(), swipeDown(), swipeRight(), swipeLeft())
swipe를 위한 액션도 존재한다.
이역시 위의 클릭, 포커싱과 동일하게 좌표를 기반으로 동작하기에
스와이프가 불가능한 뷰를 설정해도 백그라운드가 스와이프 되는 걸 확인할 수 있다.
6. addGlobalAssertion, removeGlobalAssertion
val viewAssertion = matches(withId(R.id.button))
addGlobalAssertion("button always isDisplayed",viewAssertion)
removeGlobalAssertion(viewAssertion)
화면에서 특정 조건이 불변해야 하는 뷰에 대한 처리도 가능하다.
매번 체크하는 것은 매우 귀찮기에 addGlobalAssertion
로 지정해 놓으면 모든 perform
이 끝날 때마다 자동으로 실행된다.
removeGlobalAssertion
를 이용해서 취소시킬 수 도 있다.
위의 예제는 버튼이 화면에 계속 보이고 있는지를 체크하는 코드이다.
7. repeatedlyUntil
onView(withId(R.id.button))
.perform(repeatedlyUntil(click(), hasSibling(withText("Welcome")), 10))
클릭과 같은 행동을 계속할 수는 없으니 반복할 수 있는 방법도 존재한다.
repeatedlyUntil
함수는 인자로 액션, 조건, 횟수를 받는다.
특정 횟수가 되거나 특정 조건이 만족할 때까지 계속 액션을 실행시키고, 횟수 안에 조건에 만족하지 못할 시 에러가 발생한다.
조건이 만족되면 횟수에 상관없이 바로 통과된다.
위의 예제는 button을 누르면 textView의 텍스트가 Welcome으로 변하는 코드를 테스트한 것이다.
왜 hasSibling
을 넣었는가?
조건의 대상이 자기 자신으로 한정되기 때문이다. ( 없다면, 무의미하게 button에게 "Welcome" 텍스트가 있는지만 테스트하게 된다. )
때문에, hasSibling
과 같은 관계를 이용하는 함수를 통해 다른 뷰가 조건의 대상에 포함되도록 해야 한다.
8. 그 외..
closeSoftKeyboard
, pressImeActionButton
, pressKey
, pressBack
와 키보드, 키 관련 같은 함수와openLink
, openLinkWithUri
와 같은 링크 관련 함수도 있다.
ViewAssertions
뷰의 테스트에 대한 코드가 모여있다.
1. matches
onView(withId(R.id.textView))
.check(matches(not(withText("Hello World!"))))
matches
는 위에서 계속 나왔던 코드이다. true 일 경우 테스트가 통과된다.
false 일 경우 통과되도록 지정하는 코드는 따로 없다.
대신 not()
사용하면 동일한 기능을 할 수 있다.
2. doesNotExist
onView(withText("Barabarabarabam"))
.check(doesNotExist())
doesNotExist
은 해당 뷰가 없을 때 통과되는 테스트이다.
화면에 안 보이는 게 아닌 진짜 조건에 만족하는 대상이 없어야만 통과된다.
3. selectedDescendantsMatch
onView(withId(R.id.linearLayout))
.check(
selectedDescendantsMatch(
isAssignableFrom(TextView::class.java),
hasContentDescription()
)
)
같은 조건을 가지는 여러 뷰에 대해 테스트를 진행할 수 도 있다.
selectedDescendantsMatch
을 통해서 가능하며, 첫 번째 인자로는 대상을 찾고, 두 번째 인자로 테스트를 진행한다.
위의 예제는 linearLayout 안의 TextView들이 모두 contentDescription을 가지는 테스트한 코드이다.
마무리
위의 내용으로 에스프레소의 여러 함수들과 주의사항을 살펴보았다.
#3 에서는 RecyclerView에서 어떤 식으로 테스트하면 되는지를 다루려고 한다.
'안드로이드' 카테고리의 다른 글
Android Espresso #4 - ActivityRules (0) | 2020.06.29 |
---|---|
Android Espresso #3 - RecyclerView (0) | 2020.06.26 |
Android Espresso #1 - 시작 (0) | 2020.06.24 |
[And] navigation #2 - 딥 링크 (0) | 2020.06.24 |
[And] navigation #1 - 기본 사용법 (1) | 2020.06.18 |