HOME > android > jetpack

Android Espresso의 Custom Matcher 구현 방법

JSFollow18 Nov 2018

Custom Matcher

지난 글 AndroidX Espresso Matcher 분석에서 Matcher가 어떻게 동작하는지 코드 분석을 했었습니다. 이번에는 Custom Matcher를 직접 구현해보겠습니다. 이해를 돕기 위해 TextView의 text를 비교하는 Custom Matcher를 구현해보겠습니다. ViewMatchers.withText()와 동일한 기능을 합니다.

기본 프로젝트 코드는 GitHub에서 다운받아주세요. Android Espresso로 UI 테스트 (1) 글에서 사용한 샘플코드입니다.

구현

androidTest 폴더에 CustomMatcher.kt를 생성하고 아래 코드를 입력해주세요.

withText()의 인자로 String 또는 ResourceId가 전달될 수 있기 때문에,
두개를 각각 다르게 처리할 수 있도록 TextStringMatcher와 TextResourceMatcher 클래스를 생성하였습니다. String의 경우 Matcher<String>으로 변환하고 이미 구현된 로직으로 처리하였고, ResourceId인 경우 String을 가져와 직접 equals()로 비교하도록 구현했습니다.

object CustomMatcher {

    fun withText(substring: String): Matcher<View> {
        return withText(`is`(substring))
    }

    fun withText(resourceId: Int): Matcher<View> {
        return TextResourceMatcher(resourceId)
    }

    private fun withText(stringMatcher: Matcher<String>): Matcher<View> {
        checkNotNull(stringMatcher)
        return TextStringMatcher(stringMatcher)
    }

    private class TextStringMatcher(private val stringMatcher: Matcher<String>)
            : BoundedMatcher<View, TextView>(TextView::class.java) {
        public override fun matchesSafely(view: TextView): Boolean {
            val text: String = view.text.toString()
            return text != null && stringMatcher.matches(text)
        }

        override fun describeTo(description: Description) {
            description.appendText("with text: ")
            stringMatcher.describeTo(description)
        }
    }

    private class TextResourceMatcher(private val resourceId: Int)
            : BoundedMatcher<View, TextView>(TextView::class.java) {
        private var expectedText: String? = null

        public override fun matchesSafely(view: TextView): Boolean {
            expectedText = view.resources.getString(resourceId)
            val actualText: String = view.text.toString()
            return actualText != null && actualText.equals(expectedText)
        }

        override fun describeTo(description: Description) {
            if (expectedText != null) {
                description.appendText("with text: $expectedText")
            }
        }
    }
}

Custom Matcher가 잘 만들어졌는지 테스트 코드를 작성해보겠습니다. 아래 코드처럼 먼저 인자로 String을 직접 전달하였고, 그 다음에는 ResourceId를 인자로 전달해보았습니다.

테스트를 실행해보니 모두 패스하였습니다. 자잘한 버그가 있을지 모르겠지만, ViewMatchers.withText()와 동일한 기능을 하는 Custom Matcher가 완성되었습니다!

@RunWith(AndroidJUnit4::class)
@LargeTest
class CustomMatcherTest {
    @Rule
    @JvmField
    var activityRule = ActivityTestRule(LocaleActivity::class.java)

    @Test
    fun noCountryExtra() {
        activityRule.launchActivity(Intent())
        Espresso.onView(withId(R.id.tvLocale))
            .check(matches(CustomMatcher.withText("No country string")))

        Espresso.onView(withId(R.id.tvLocale))
            .check(matches(CustomMatcher.withText(R.string.no_country_extra)))
    }
}

정리

AndroidX Espresso Matcher 분석에서 Matcher의 코드를 분석해보았고, 이번에는 Custom Matcher를 만들어보았습니다. 기본적인 Matcher는 이미 만들어져 있기 때문에, 추가로 만들 일이 별로 없을 것 같습니다. 에스프레소의 내부 구조를 이해하는데 도움이 되었으면 좋겠습니다.

참고