Android Espresso를 사용하여 UI를 테스트하는 방법 (2)

이번 글에서는 에스프레소의 다음 내용을 다룹니다.

  • MainActivity, SecondActivity 테스트
  • Intents.intending과 Intents.intended

기본적인 에스프레소 사용방법은 이전 글을 참고해주세요.

이 글은 Android Espresso를 사용하여 UI를 테스트하는 방법 (1)에 이어지는 2번째 글입니다.

MainActivity 테스트 코드 작성

MainActivity의 코드는 매우 간단합니다. FAB버튼을 누르면 LocaleActivity가 실행되고 EXTRA_COUNTRY가 "korea"인 Intent가 전달됩니다. LocaleActivity가 종료되면 result로 Locale정보를 받고 MainActivity의 tvResult에 출력해줍니다.

override fun onCreate(savedInstanceState: Bundle?) {
    ....
    fab.setOnClickListener { view ->
        val intent = Intent(this, LocaleActivity::class.java)
        intent.putExtra(LocaleActivity.EXTRA_COUNTRY, getString(R.string.country_korea))
        startActivityForResult(intent, REQUEST_CODE_LOCALE)
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
        REQUEST_CODE_LOCALE -> {
            val locale = data?.getStringExtra(LocaleActivity.EXTRA_LOCALE)
            tvResult.text = locale
        }
    }
}

androidTest폴더에 MainActivityTest.kt를 생성하고 아래처럼 기본적인 내용을 입력해줍니다. LocaleActivityTest는 ActivityTestRule을 정의하였는데, 여기서는 Intents를 사용하기 때문에 IntentsTestRule를 정의해야 합니다. (IntentsTestRule는 ActivityTestRule을 상속합니다. 자세한 것은 Developers: IntentsTestRule을 참고해주세요)

@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest {
    @Rule
    @JvmField
    var activeRule = IntentsTestRule(MainActivity::class.java)

}

첫번째 테스트

FAB버튼을 눌렀을 때 LocalActivity가 실행되고 Locale이 "ko-kr"로 화면에 출력되는지 확인하려고 합니다.

아래 코드처럼 입력해주세요. perform(click())은 지정한 View를 클릭하라는 의미입니다. FAB버튼을 클릭하면 LocaleActivity가 실행됩니다. R.id.tvLocale은 LocaleActivity에 있는 TextView이고 "ko-kr"이 출력됩니다. 테스트 코드로 "ko-kr"이 출력되는지 확인했습니다.

테스트를 실행해보세요.

@Test
fun launchActivity() {
    onView(withId(R.id.fab))
            .perform(click())
    onView(withId(R.id.tvLocale))
            .check(matches(withText(R.string.locale_korea)))
}

Intended

지금 테스트하고 있는 내용은 모두 자신이 구현한 Component인 MainActivity, LocaleActivity입니다. 코드가 프로젝트 내에 있기 때문에 올바른 인텐트를 주었는지, 올바른 인텐트를 받았는지 확인할 수 있습니다.

하지만 만약 MainActivity에서 Contacts(연락처)앱의 Activity를 실행하여 정보를 가져온다면, Contacts가 올바른 Intent를 받았는지 테스트하기 힘들 수 있습니다. Component가 내 앱에 구현되어있지 않고 다른 앱 내에 있기 때문에요.

Intended는 이런 테스트를 하는데 도움을 주는 기능입니다. MainActivity에서 인텐트를 전달할 때 이것을 가로채서 의도된 인텐트인지 확인을 합니다.

두번째 테스트

Intended를 이용하여 MainActivity에서 LocaleActivity로 올바른 인텐트를 전달하는지 테스트 코드를 작성해봅시다. 아래 코드는 FAB버튼을 눌렀을 때 MainActivity에서 전달하는 인텐트가 의도된 인텐트인지 확인하는 테스트 코드입니다.

intended(...) 안에도 Matcher가 들어가 있는데요. hasExtra()는 인텐트의 EXTRA 데이터가 일치하는지 확인합니다. LocaleActivity로 전달되는 인텐트는 "korea"를 포함하고 있기 때문에 아래 코드는 문제 없이 패스하게 됩니다.

toPackage(...)는 인텐트가 어떤 패키지로 전달되는지 확인하는 Matcher입니다. LocaleActivity의 PackageName도 com.codechacha.espresso이기 때문에 저 코드도 패스하게 됩니다.

hasComponent(...)는 인텐트를 수신하는 Component를 체크하는데 사용하는 Matcher입니다. MainActivity를 보시면 인텐트를 생성할 때 LocaleActivity를 지정해주는 부분이 있습니다. 여기서 Component가 LocaleActivity로 설정되었습니다. (지금 코드는 명시적 인텐트인데요, 암시적 인텐트의 경우 ActivityManager에서 Intent를 resovling하고 Component를 지정해줍니다)

allOf(..)은 3개의 intended로 나눠 작성한 것을 하나의 intended로 작성할 수 있게 조건을 묶어 줍니다.

@Test
fun localeIntended() {
    onView(withId(R.id.fab))
            .perform(click())

    val context = InstrumentationRegistry.getInstrumentation().targetContext
    val country = context.getString(R.string.country_korea)

    intended(hasExtra(LocaleActivity.EXTRA_COUNTRY, country))
    intended(toPackage("com.codechacha.espresso"))
    intended(IntentMatchers.hasComponent(
            "com.codechacha.espresso.LocaleActivity"))

    intended(allOf(
        hasExtra(LocaleActivity.EXTRA_COUNTRY, country),
        toPackage("com.codechacha.espresso"),
        IntentMatchers.hasComponent("com.codechacha.espresso.LocaleActivity")))
}

Intending

Indending를 사용하는 이유도 Intended와 유사하게, 인텐트가 전달되는 앱이 내 앱이 아닌 경우 테스트하기가 어렵기 때문입니다.

하지만 LocaleActivity가 올바른 인텐트를 받았는지를 체크하는 Intended와 다르게 Intending은 MainActivity로 올바른 인텐트가 전달되고 올바르게 처리되는지 확인하는데 사용됩니다. Intending은 LocaleActivity가 인텐트를 어떻게 처리했는지를 중요하게 생각하지 않습니다. LocalActivity가 어떤 인텐트를 결과로 리턴할 때 MainActivity가 의도한대로 처리되는지 테스트하는데 초점을 맞춥니다.

아래처럼 코드를 작성하였습니다. 이 코드는 FAB버튼을 눌렀을 때 LocaleActivity가 Locale정보를 담은 인텐트를 리턴하고 MainActivity에서 의도된 대로 처리되는지 확인합니다.

intending은 안드로이드 테스트 라이브러리인 Mockito의 when과 유사합니다. Activity를 실행할 때 결과로 어떤 인텐트가 리턴되는지 스펙을 미리 정할 수 있습니다. 이런 이유로, LocaleActivity 내부적으로 인텐트를 어떻게 처리하는지 중요하지 않습니다. 왜냐하면 결과로 리턴받는 인텐트를 직접 설정할 수 있기 때문입니다.

결과로 인텐트를 전달받으면, MainActivity 내에서 이 인텐트를 처리하고 View에 결과물을 출력해줍니다. onView(...)를 이용하여 의도된 결과를 출력하는지 확인하면 됩니다.

리턴 받을 인텐트를 생성하고, ActivityResult 객체를 생성합니다. intending(..).respondWith(result)가 호출되면 MainActivity에서 전달되는 인텐트에 대해서 결과 인텐트로 우리가 생성한 인텐트를 리턴해줍니다. intending(..)에 Matcher가 있는 것을 볼 수 있는데요. toPackage("com.codechacha.espresso")로 입력하면 com.codechacha.espresso 패키지로 전달되는 인텐트에 한해서 respondWith()에 입력한 인텐트가 결과로 리턴하겠다는 의미입니다.

LocaleActivity에서 리턴될 것이라고 예상되는 인텐트를 모두 설정하고, perform(click())을 호출하면 설정한 인텐트가 리턴됩니다. 실제로 LocaleActivity를 실행하지 않기 때문에 LocaleActivity에서 OK버튼을 누를 필요도 없습니다. "ko-kr"이 인텐트로 전달되기 때문에 R.id.tvResult에 "ko-kr"이 출력됩니다. check(...)로 예상 결과와 일치하는지 확인합니다.

세번째 테스트

@Test
fun localeIntending() {
    val context = InstrumentationRegistry.getInstrumentation().targetContext
    val locale = context.getString(R.string.locale_korea)

    val intent = Intent()
    intent.putExtra(LocaleActivity.EXTRA_LOCALE, locale)
    val result =
            Instrumentation.ActivityResult(Activity.RESULT_OK, intent)

    intending(toPackage("com.codechacha.espresso"))
            .respondWith(result)

    onView(withId(R.id.fab))
            .perform(click())
    onView(withId(R.id.tvResult))
            .check(matches(withText(locale)))
}

SecondActivity 테스트 코드 작성

SecondActivity는 MainActivity와 거의 동일합니다. MainActivity에는 EXTRA가 "korea"로 하드코딩되어있지만, SecondActivity는 EditText를 통해 입력을 받아 전달한다는 것이 다릅니다.

androidTest폴더에 SecondActivityTest.kt를 생성하고 기본적인 내용들을 입력해줍니다.

@RunWith(AndroidJUnit4::class)
class SecondActivityTest {
    @Rule
    @JvmField
    var activeRule = ActivityTestRule(SecondActivity::class.java)

}

첫번째 테스트

사람이 손으로 EditText에 "korea"를 입력하고 눈으로 결과를 확인하는 단순 작업을 코드로 대신하려고 합니다.

아래 처럼 코드를 입력하면, EditText에 "korea"를 입력하고... 올바른 결과가 리턴되었는지 확인할 수 있습니다.

perform(typeText(inputStr)는 인자로 전달된 변수를 EditText에 입력하라는 의미입니다. closeSoftKeyboard()는 SoftKeyboard가 열려 있으면 닫으라는 의미입니다.

FAB버튼을 누르면 LocaleActivity가 실행되는데요. 이 때 올바른 Locale이 출력되고 있는지 확인합니다.

OK버튼이 눌리면 MainActivity로 Locale정보를 포함한 인텐트가 전달됩니다. MainActivity는 이 인텐트를 받고 결과를 출력해줍니다. 출력이 의도된대로 되었는지 확인합니다.

@Test
fun userInputTest() {
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    val inputStr = context.getString(R.string.country_korea)
    onView(withId(R.id.editTextUserInput))
            .perform(typeText(inputStr), closeSoftKeyboard())

    onView(withId(R.id.fab))
            .perform(click())

    onView(withId(R.id.tvLocale))
            .check(matches(withText(R.string.locale_korea)))

    onView(withId(R.id.btnOk))
        .perform(click())

    onView(withId(R.id.tvResultLocale))
        .check(matches(withText(R.string.locale_korea)))
}

정리

MainActivity 테스트에서는 Intended와 Intending을 이용하여 테스트 코드를 작성하는 방법에 대해서 알아보았습니다. 기억해야 할 것은 이 두개의 기능이 Component의 의존성을 끊고 각각을 독립적으로 테스트할 수 있도록 도와준다는 것입니다.

SecondActivity 테스트에서는 EditText가 있을 때 테스트하는 방법에 대해서 알아보았습니다.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha