반응형

Espresso-Intents 패키지는 테스트할 때 Intent에 대해 stubbing을 할 수 있게 지원 해줍니다.
쉽게 말해 Android Intent를 위한 Mockito 역할을 한다고 보면 되겠습니다.

 

테스트할 앱이 다른 앱이나 플랫폼에 기능을 위임하는 경우 Espresso-Intents 를 사용하여 다른 앱이나 플랫폼이 올바르게 작동한다고 가정할 수 있습니다. 또, 테스트 앱으로부터 나가는 내용을 stubbing 하거나 검증할 수 있으며 실제 Intent 응답 값 대신 stub intent response 를 받을 수 있습니다.

 

Setup

앱 수준의 build.gradle 파일에 다음과 같이 디펜던시를 추가해줍니다.

dependencies {
    //...
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'
}

 

Write Test Rules

Espresso-Intents 테스트를 하기에 앞서 IntentsTestRule 을 세팅해야 합니다.
이는 ActivityTestRule 의 확장이며, 이는 Espresso-Intents API를 사용하여 UI Test 를 보다 쉽게 해줍니다.

IntentsTestRule@Test 어노테이션이 붙은 각 테스트를 테스트하기에 앞서 매번 초기화 됩니다. Unit Test 에서 @Before 의 기능과 유사하다고 보면 되겠습니다.

@Rule
public IntentsTestRule<MyActivity> intentsTestRule =
    new IntentsTestRule<>(MyActivity.class);

 

Intent Test

인텐트를 테스트할 때에도 역시나 Hamcrest Matchers 를 적극 활용합니다. Hamcrest 를 사용하여 인텐트를 테스트하는 데에는 다음 두 가지로 나눌 수 있습니다.

  • 이미 존재하는 인텐트 matcher 사용 (가장 많이 사용 되는 방법)
  • 커스텀 인텐트 matcher 구현 - 유연하게 hamcrest matcher 를 사용하여 필요에 따라 matcher 커스텀

 

두 번째 방법인 커스텀 인텐트 matcher 의 경우 Espresso-Intents 에서 제공하는 Intended() 와 Intending() 을 사용하여 인텐트를 validation 및 stubbing 할 수 있습니다.

 

첫 번째 방법은 아래 예시와 같이 사용됩니다.

assertThat(intent).hasAction(Intent.ACTION_VIEW);
assertThat(intent).categories().containsExactly(Intent.CATEGORY_BROWSABLE);
assertThat(intent).hasData(Uri.parse("www.google.com"));
assertThat(intent).extras().containsKey("key1");
assertThat(intent).extras().string("key1").isEqualTo("value1");
assertThat(intent).extras().containsKey("key2");
assertThat(intent).extras().string("key2").isEqualTo("value2");

보시다시피 인텐트의 Action, Category, Extra 등 다양한 값들을 검증할 수 있습니다.

 

참고로, MockitoAnnotations.initMocks 처럼 인텐트를 검증할 때에는 @Before 단계에서 init() 메소드를 통해 초기화 해야합니다. 그리고 각 테스트가 끝날때마다 release() 해줘야 합니다.

 

Intended

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

 

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

 

intended()Mockito.verify() 와 비슷하다고 보면 됩니다. 테스트 작성자는 이를 통해 주어진 인텐트를 검증할 수 있습니다.

 

intended() 메소드는 다음 두 가지 종류가 있습니다.

  • void intended(Matcher<Intent> matcher, VerificationMode verificationMode)

    이는 Mockitoverify(mock, times(num)) 과 유사합니다.
    주어진 횟수와 일치하는 수의 intent 가 matcher 에 부합 하는지를 검증합니다.

    이때 검증은 인텐트가 전송된 것과 동일한 순서로 수행되지 않아도 됩니다.
    Intents.init() 이 호출된 시점부터 기록됩니다.

  • void intended(Matcher<Intent> matcher)

    주어진 matcher와 테스트하는 앱의 인텐트가 오직 한 번만 부합 하는 것을 검증합니다.

    이는 Mockitoverify(mock, times(1)) 과 같습니다.

    마찬가지로 인텐트가 전송된 것과 동일한 순서로 수행되지 않아도 되며 Intents.init() 이 호출된 시점부터 기록됩니다.

 

FirstActivity 의 버튼을 클릭 했을 때 SecondActivity 로 전환되면서
Intent 에 Extra 값(Key : SimpleActivity.FOO, Val : "foo")을 담아서 전달하는 앱이 있다는 가정 하에 간단한 테스트를 작성해보겠습니다.

@Test
public void simpleIntended() {
    onView(withId(R.id.button_sample))
            .perform(click())

    Context context = InstrumentationRegistry
                                                    .getInstrumentation()
                                                    .getTargetContext();
    String foo = context.getString(R.string.foo);

    intended(hasExtra(SecondActivity.FOO, foo));
    intended(toPackage("com.ready.uitest"));
    intended(IntentMatchers.hasComponent(
            "com.ready.uitest.SecondActivity"));

    intended(allOf(
        hasExtra(SecondActivity.FOO, foo),
        toPackage("com.ready.uitest"),
        IntentMatchers.hasComponent("com.ready.uitest.SecondActivity")));
}

위 코드처럼 FirstActivity 에서 SecondActivity 로 전환하면서 전달된 인텐트에 대해서 테스트할 수 있었습니다.

 

하지만, intended 는 보내는 인텐트에 대해서만 테스트 할 수 있을 뿐이지 다른 안드로이드 컴포넌트로부터 응답 받은 인텐트를 확인할 수는 없습니다. 따라서 받은 인텐트에 대해 검증하고 싶을 때에는 intended() 가 아닌 intending()을 사용해야 합니다.

 

Intending

intending() 메소드는 Mockito.when() 과 유사합니다. 이를 통해 테스트 작성자는 startActivityForResult() 로 실행된 액티비티의 응답을 stubbing 할 수 있습니다.

 

테스트 하는 액티비티의 외부 액티비티 UI 에 대해서는 다룰 수 없기 때문에 ActivityResult 로 받아오는 결과에 대해 다룰 수 있게 해주는 intending() 메소드가 유용하게 사용됩니다.

 

테스트 작성자는 intending(matcher) 가 리턴하는 OngoingStubbing 객체를 통해 추가적으로 메소드를 이어붙여 launching 액티비티가 올바르게 결과를 다루는지도 확인할 수 있습니다.

  • intending(Matcher matcher).respondWith(Instrumentation.ActivityResult result)
  • intending(Matcher matcher).respondWithFunction(ActivityResultFunction result)

 

앞서 Intended 에서 살펴본 예제를 수정하여 SecondActivity 에서 버튼을 클릭하면 FirstActivity 로 결과를 반환하도록 구현했다고 가정하겠습니다.

 

이때 intending()SecondActivityFirstActivity 로부터 인텐트를 받아 어떻게 처리했는지를 중요하게 생각하지 않습니다. 오직 SecondActivity 가 인텐트를 결과로 리턴할 때 FirstActivity 가 의도한대로 처리되는지 테스트 하는데 초점이 맞춰져 있습니다.

 

SecondActivity 가 내부적으로 인텐트를 어떻게 처리하는지 중요하지 않습니다. 왜냐하면 결과로 리턴 받는 인텐트를 직접 설정할 수 있기 때문입니다. 결과로 인텐트를 전달 받으면 FirstActivity 내에서 이 인텐트를 처리하고 View 에 결과물을 출력해줍니다. 그리고 onView(...) 를 이용하여 의도된 결과를 출력하는지 확인합니다.

 

그럼 FirstActivity를 기준으로 한 테스트 코드를 작성해보겠습니다.

@Test
public void simpleIntending() {
    Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
    String foo = context.getString(R.string.locale_korea);

    Intent intent = new Intent();
    intent.putExtra(FirstActivity.FOO, foo);
    Instrumentation.ActivityResult result =
            new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);

    intending(toPackage("com.ready.uitest"))
            .respondWith(result);

    onView(withId(R.id.button_sample)).perform(click());
    onView(withId(R.id.text_result)).check(matches(withText(foo)));
}

SecondActivity 에서 리턴될 것이라고 예상되는 인텐트를 모두 설정하고, perform(click()) 을 호출하면 설정한 인텐트가 리턴됩니다.

 

실제로 SecondActivity 를 실행하지 않기 때문에 SecondActivity 에서 버튼을 누를 필요도 없습니다. "foo" 가 인텐트로 전달되기 때문에 R.id.text_result 에 "foo" 가 출력될 것이고, check(...) 로 예상 결과와 일치하는지 확인합니다.

 


이렇게 Intended 와 Intending 을 이용하여 테스트 코드를 작성하는 방법에 대해서 알아보았습니다.

기억해야 할 것은 이 두개의 기능이 안드로이드 컴포넌트의 의존성을 끊고 각각 독립적으로 테스트할 수 있도록 도와준다는 것입니다.

반응형
반응형