Kotlin으로 ☕ Espresso UI 테스트 기반 잘 다지는 법 #2 - Matchers
반응형
🏤 나만의 관리소 만들기
Espresso
에서 Matcher
란 어플리케이션 내 element들의 패턴을 해석하여 원하는 작업을 수행하는 엔진이다. 이는 기본 Espresso 라이브러리에서 곧장 불러다가 사용이 가능하지만, 매번 패키지를 import 하고, 여러 가지 작업을 연달아 수행하다 보면 코드가 굉장히 지저분하고 길어질 수 있다. 베이스 Interface를 생성하여 이를 관리해주면, 테스트 코드가 보다 보기 쉽고 간편해지며, 관리 또한 수월해진다.
BaseViewMatchers.kt
interface BaseViewMatchers {
fun onView(viewMatcher: Matcher<View>): ViewInteraction = Espresso.onView(viewMatcher)
fun onData(viewMatcher: Matcher<View>): DataInteraction = Espresso.onData(viewMatcher)
fun withId(id: Int): Matcher<View> = ViewMatchers.withId(id)
fun hasFocus(): Matcher<View> = ViewMatchers.hasFocus()
fun findElementById(resourceId: Int) = onView(withId(resourceId))
fun findElementByText(text: String) = onView(ViewMatchers.withText(text))
fun findElementByText(resourceId: Int) = onView(ViewMatchers.withText(resourceId))
fun findElementByContentDescription(text: String) =
onView(ViewMatchers.withContentDescription(text))
fun findElementByIdWithParent(
resourceId: Int,
parentResourceId: Int
) = onView(allOf(withId(resourceId),
ViewMatchers.isDescendantOfA(allOf(withId(parentResourceId)))))
val isDialog: Matcher<Root>
get() = androidx.test.espresso.matcher.RootMatchers.isDialog()
val isDisplayed: ViewAssertion
get() = ViewAssertions.matches(ViewMatchers.isDisplayed())
val isCompletelyDisplayed: ViewAssertion
get() = ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed())
val isNotDisplayed: ViewAssertion
get() = ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isDisplayed()))
val isVisible: ViewAssertion
get() = ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
val isInvisible: ViewAssertion
get() = ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.INVISIBLE))
val isGone: ViewAssertion
get() = ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.GONE))
val isEnabled: ViewAssertion
get() = ViewAssertions.matches(ViewMatchers.isEnabled())
fun withIndex(
matcher: Matcher<View>,
index: Int
) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.run {
appendText("with index: ")
appendValue(index)
}
matcher.describeTo(description)
}
public override fun matchesSafely(view: View) =
matcher.matches(view) && index == 1
}
}
위와 같이 기본적으로 자주사용하는 matcher들을 정리해주면, 앞으로 테스트 작성 시 사용 및 관리가 매우 수월해진다. 기본 라이브러리로 1번처럼 쓰이던 코드가 더욱 짧고 읽기 쉽게 2번처럼 변형되어 사용되는 것이다.
1. onView(withId(R.id.resource_id)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
2. findElementById(R.id.resource_id).check(isDisplayed)
이 베이스에서 한단계 진화해, 좀 더 복잡한 그렇지만 자주 사용되는 체크들을 간결화할 수도 있다.
CollectedViewMatchers.kt
interface CollectedViewMatchers : BaseViewMatchers {
fun findElementByIdWithParent(
resourceId: Int,
parentResourceId: Int
) =
onView(allOf(withId(resourceId), isDescendantOfA(allOf(withId(parentResourceId)))))
fun findElementByContentDescription(text: String) =
onView(withContentDescription(text))
fun isElementByIdEnabled(resourceId: Int) =
findElementById(resourceId).check(isEnabled)
fun isElementByIdAppeared(resourceId: Int) =
findElementById(resourceId).check(isDisplayed)
fun isElementByIdCompletelyDisplayed(resourceId: Int) =
findElementById(resourceId).check(isCompletelyDisplayed)
fun isElementByIdWithParentDisplayed(resourceId: Int, parentResourceId: Int) =
findElementByIdWithParent(resourceId = resourceId, parentResourceId = parentResourceId).check(isDisplayed)
fun isElementByIdHaveSiblingWithId(resourceId: Int, siblingResourceId: Int) =
runCatching {
onView(allOf(withId(resourceId), isCompletelyDisplayed()))
.check(matches(hasSibling(allOf(withId(siblingResourceId), isCompletelyDisplayed()))))
}.isSuccess
fun isElementByIdAppearedAtLeast(resourceId: Int, elementAreaPercentage: Int) =
runCatching {
onView(allOf(withId(resourceId), isDisplayingAtLeast(elementAreaPercentage)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}.isSuccess
fun isElementByIdHasText(
resourceId: Int,
text: String
) {
runCatching {
findElementById(resourceId).check(matches(withText(equalToIgnoringCase(text))))
}.onFailure {
onData(allOf(withId(resourceId))).atPosition(0)
.check(matches(withText(equalToIgnoringCase(text))))
}
}
fun isElementByIdDisappeared(resourceId: Int) {
with(findElementById(resourceId)) {
runCatching {
check(doesNotExist())
}.onFailure {
runCatching {
check(isInvisible)
}.onFailure {
check(isGone)
}
}
}
}
fun getTextFromElementByID(resourceId: Int): String {
var text: String? = null
onView(withId(resourceId)).perform(object : ViewAction {
override fun getConstraints() = isAssignableFrom(TextView::class.java)
override fun getDescription() = "getting text from a TextView"
override fun perform(uiController: UiController, view: View) {
text = (view as TextView).text.toString()
}
})
return text!!
}
}
또한, 자주 사용되는 액션들을 관리해줄수도 있다.
ActionViewMatchers.kt
interface ActionViewMatchers : BaseViewMatchers {
private fun safeClick(viewInteraction: ViewInteraction) {
with(viewInteraction) {
runCatching {
perform(scrollTo(), click())
}.onFailure {
perform(click())
}
}
}
fun tapById(resourceId: Int) = safeClick(findElementById(resourceId))
fun tapByIdAndText(
resourceId: Int,
text: String
) = safeClick(findElementById(resourceId).check(matches(withText(equalToIgnoringCase(text)))))
fun tapByIdWithParent(
resourceId: Int,
parentResourceId: Int
) = safeClick(findElementByIdWithParent(resourceId, parentResourceId))
fun tapByIdAndTextWithParent(
resourceId: Int,
text: String,
parentResourceId: Int
) = safeClick(
findElementByIdWithParent(resourceId, parentResourceId)
.check(matches(withText(equalToIgnoringCase(text))))
)
fun tapByText(text: String) = safeClick(findElementByText(text))
fun tapByContentDescription(contentDescription: String) =
safeClick(findElementByContentDescription(contentDescription))
fun tapByLocation(x: Int, y: Int) =
GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val screenPosition = IntArray(2).apply {
view?.getLocationOnScreen(this)
}
val screenX = (screenPosition[0] + x).toFloat()
val screenY = (screenPosition[1] + y).toFloat()
floatArrayOf(screenX, screenY)
},
Press.FINGER,
InputDevice.SOURCE_MOUSE,
MotionEvent.BUTTON_PRIMARY
)
fun tapByLocationPercentage(xPercentage: Double, yPercentage: Double) =
GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val screenPosition = IntArray(2).apply {
view?.getLocationOnScreen(this)
}
val x = view.width * xPercentage
val y = view.height * yPercentage
val screenX = (screenPosition[0] + x).toFloat()
val screenY = (screenPosition[1] + y).toFloat()
floatArrayOf(screenX, screenY)
},
Press.FINGER,
InputDevice.SOURCE_MOUSE,
MotionEvent.BUTTON_PRIMARY
)
fun swipeUp(speed: Swipe = Swipe.FAST) = GeneralSwipeAction(
speed,
GeneralLocation.CENTER,
GeneralLocation.TOP_CENTER,
Press.FINGER
)
fun swipeDown(speed: Swipe = Swipe.FAST) = GeneralSwipeAction(
speed,
GeneralLocation.CENTER,
GeneralLocation.BOTTOM_CENTER,
Press.FINGER
)
fun swipeRight(speed: Swipe = Swipe.FAST) = GeneralSwipeAction(
speed,
GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER_RIGHT,
Press.FINGER
)
fun swipeLeft(speed: Swipe = Swipe.FAST) = GeneralSwipeAction(
speed,
GeneralLocation.CENTER_RIGHT,
GeneralLocation.CENTER_LEFT,
Press.FINGER
)
}
이제 matcher class들은 extend해서 사용하면 된다
interface Util : CollectedViewMatchers, ActionViewMatchers, TestData {
...
}
이 외에도 많이 쓰이는 matcher는 알림창이 있다.
DialogViewMatchers.kt
interface DialogViewMatchers : BaseViewMatchers {
fun isDialogWithTitleAppeared(title: String): ViewInteraction =
findElementByText(title).inRoot(isDialog).check(isDisplayed)
fun isDialogWithTextDisplayed(text: String): Boolean =
runCatching { isDialogWithTitleAppeared(text) }.isSuccess
fun waitForDialogWithTextToBeDisplayed(text: String) {
do {
SystemClock.sleep(10)
} while (!isDialogWithTextDisplayed(text))
}
fun tapButtonOnDialog(buttonLabel: String): ViewInteraction =
findElementByText(buttonLabel)
.inRoot(isDialog)
.check(isEnabled)
.perform(click())
fun tapButtonOnDialog(resourceId: Int): ViewInteraction =
findElementById(resourceId)
.inRoot(isDialog)
.check(isEnabled)
.perform(click())
}
이런식으로 matcher들을 나눠서 잘 관리하면, 깔끔한 테스트 코드를 작성할 수 있고, dependency 관리 또한 쉬워진다.
반응형
'👨🏻💻 QA이야기 > 📱 모바일자동화' 카테고리의 다른 글
Kotlin으로 ☕ Espresso UI 테스트 기반 잘 다지는 법 #1 - 테스트 케이스 (0) | 2020.04.12 |
---|---|
Swift로 🍏 UI XCTest 기반 잘 다지는 법 #2 - Action (0) | 2020.04.04 |
Swift로 🍏 UI XCTest 기반 잘 다지는 법 #1 - 테스트 케이스 (0) | 2020.04.04 |
🥒 Cucumber 이해하고 잘 쓰는 방법 (0) | 2020.04.03 |
🗳️ QA의 모바일 자동화를 위한 개발환경 (0) | 2020.04.01 |
댓글
이 글 공유하기
다른 글
-
Kotlin으로 ☕ Espresso UI 테스트 기반 잘 다지는 법 #1 - 테스트 케이스
Kotlin으로 ☕ Espresso UI 테스트 기반 잘 다지는 법 #1 - 테스트 케이스
2020.04.12 -
Swift로 🍏 UI XCTest 기반 잘 다지는 법 #2 - Action
Swift로 🍏 UI XCTest 기반 잘 다지는 법 #2 - Action
2020.04.04 -
Swift로 🍏 UI XCTest 기반 잘 다지는 법 #1 - 테스트 케이스
Swift로 🍏 UI XCTest 기반 잘 다지는 법 #1 - 테스트 케이스
2020.04.04 -
🥒 Cucumber 이해하고 잘 쓰는 방법
🥒 Cucumber 이해하고 잘 쓰는 방법
2020.04.03