Espresso 기본사항

이 문서에서는 Espresso API를 사용하여 일반적인 자동 테스트 작업을 완료하는 방법을 설명합니다.

Espresso API는 테스트 작성자가 사용자가 애플리케이션과 상호작용하는 동안 할 수 있는 작업, 즉 UI 요소를 찾고 요소와 상호작용하는 측면에서 생각하도록 권장합니다. 동시에 프레임워크는 애플리케이션의 활동 및 뷰에 직접 액세스하지 못하게 합니다. 이러한 객체를 유지하고 UI 스레드에서 벗어나 이러한 객체를 조작하는 것이 테스트 결함의 주요 원인이기 때문입니다. 따라서 getView()getCurrentActivity()와 같은 메서드는 Espresso API에 표시되지 않습니다. ViewActionViewAssertion의 자체 서브클래스를 구현하여 뷰에서 안전하게 작업할 수 있습니다.

API 구성요소

Espresso의 기본 구성요소에는 다음이 포함됩니다.

  • EspressoonView()onData()를 통한 뷰와의 상호작용을 위한 진입점입니다. 또한 어떤 뷰에도 연결되지 않아도 되는 API (예: pressBack())를 노출합니다.
  • ViewMatchersMatcher<? super View> 인터페이스를 구현하는 객체의 모음입니다. 이 중 하나 이상을 onView() 메서드에 전달하여 현재 뷰 계층 구조 내에서 뷰를 찾을 수 있습니다.
  • ViewActions: ViewInteraction.perform() 메서드에 전달할 수 있는 ViewAction 객체의 컬렉션입니다(예: click()).
  • ViewAssertions: ViewInteraction.check() 메서드에 전달할 수 있는 ViewAssertion 객체의 모음입니다. 대부분의 경우 뷰 매처를 사용하여 현재 선택된 뷰의 상태를 어설션하는 matches 어설션을 사용합니다.

예:

Kotlin

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

Java

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

뷰 찾기

대부분의 경우 onView() 메서드는 현재 뷰 계층 구조 내에서 단 하나의 뷰와 일치할 것으로 예상되는 hamcrest 매처를 사용합니다. 매처는 강력하며 Mockito 또는 JUnit과 함께 사용한 사용자에게 익숙합니다. hamcrest 매처에 익숙하지 않다면 먼저 이 프레젠테이션을 빠르게 살펴보시기 바랍니다.

종종 원하는 뷰에는 고유한 R.id가 있으며 간단한 withId 매처는 뷰 검색 범위를 좁힙니다. 그러나 테스트 개발 시 R.id를 확인할 수 없는 정당한 사례가 많이 있습니다. 예를 들어 특정 뷰에 R.id가 없거나 R.id이 고유하지 않을 수 있습니다. 이로 인해 일반적인 뷰 액세스 방식(findViewById() 사용)이 작동하지 않기 때문에 일반 계측 테스트가 불안정하고 작성하기 복잡해질 수 있습니다. 따라서 뷰를 보유하는 활동이나 프래그먼트의 비공개 멤버에 액세스하거나 알려진 R.id가 있는 컨테이너를 찾아 특정 뷰의 콘텐츠로 이동해야 할 수 있습니다.

Espresso는 기존 ViewMatcher 객체 또는 자체 맞춤 객체를 사용하여 뷰 범위를 좁힐 수 있도록 하여 이 문제를 깔끔하게 처리합니다.

R.id로 뷰를 찾는 것은 onView()를 호출하는 것만큼 간단합니다.

Kotlin

onView(withId(R.id.my_view))

Java

onView(withId(R.id.my_view));

때로 R.id 값은 여러 뷰 간에 공유됩니다. 이 경우 특정 R.id를 사용하려고 하면 AmbiguousViewMatcherException와 같은 예외가 발생합니다. 예외 메시지는 고유하지 않은 R.id와 일치하는 뷰를 검색하고 찾을 수 있는 현재 뷰 계층 구조의 텍스트 표현을 제공합니다.

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

다양한 뷰 속성을 살펴보면서 고유하게 식별 가능한 속성을 찾을 수 있습니다. 위의 예에서는 뷰 중 하나에 "Hello!" 텍스트가 있습니다. 이를 사용하여 조합 매처를 사용하여 검색 범위를 좁힐 수 있습니다.

Kotlin

onView(allOf(withId(R.id.my_view), withText("Hello!")))

Java

onView(allOf(withId(R.id.my_view), withText("Hello!")));

매처를 역전시키지 않도록 선택할 수도 있습니다.

Kotlin

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Java

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

Espresso에서 제공하는 뷰 매처는 ViewMatchers를 참고하세요.

고려사항

  • 잘 작동하는 애플리케이션에서 사용자가 상호작용할 수 있는 모든 뷰에는 설명 텍스트가 포함되거나 콘텐츠 설명이 포함되어야 합니다. 자세한 내용은 더욱 접근성 높은 앱 만들기를 참고하세요. withText() 또는 withContentDescription()를 사용하여 검색 범위를 좁힐 수 없으면 접근성 버그로 처리하는 것이 좋습니다.
  • 찾고 있는 하나의 뷰를 찾는 최소 설명 매처를 사용합니다. 프레임워크가 필요한 것보다 더 많은 작업을 하게 되므로 과도하게 지정하지 마세요. 예를 들어 뷰를 텍스트로 고유하게 식별할 수 있는 경우 TextView에서도 뷰를 할당할 수 있다고 지정할 필요가 없습니다. 많은 뷰에서 뷰의 R.id로 충분합니다.
  • 타겟 뷰가 AdapterView(예: ListView, GridView 또는 Spinner) 내에 있으면 onView() 메서드가 작동하지 않을 수 있습니다. 이러한 경우에는 onData()를 대신 사용해야 합니다.

뷰에 작업 실행

타겟 뷰에 적합한 매처를 찾았으면 perform 메서드를 사용하여 이 뷰에 ViewAction 인스턴스를 실행할 수 있습니다.

예를 들어 뷰를 클릭하려면 다음을 실행합니다.

Kotlin

onView(...).perform(click())

Java

onView(...).perform(click());

한 번의 perform 호출로 둘 이상의 작업을 실행할 수 있습니다.

Kotlin

onView(...).perform(typeText("Hello"), click())

Java

onView(...).perform(typeText("Hello"), click());

작업 중인 뷰가 ScrollView(세로 또는 가로) 내에 있으면 click()typeText()와 같이 뷰를 표시해야 하는 선행 작업(예: scrollTo())을 사용하는 것이 좋습니다. 이렇게 하면 다른 작업을 진행하기 전에 뷰가 표시됩니다.

Kotlin

onView(...).perform(scrollTo(), click())

Java

onView(...).perform(scrollTo(), click());

Espresso에서 제공하는 뷰 작업은 ViewActions를 참고하세요.

뷰 어설션 확인

check() 메서드를 사용하여 현재 선택된 뷰에 어설션을 적용할 수 있습니다. 가장 많이 사용되는 어설션은 matches() 어설션입니다. 이 클래스는 ViewMatcher 객체를 사용하여 현재 선택된 뷰의 상태를 어설션합니다.

예를 들어 뷰에 "Hello!" 텍스트가 있는지 확인하려면 다음 코드를 사용합니다.

Kotlin

onView(...).check(matches(withText("Hello!")))

Java

onView(...).check(matches(withText("Hello!")));

"Hello!"가 뷰의 콘텐츠인지 어설션하려는 경우 다음은 잘못된 사례로 간주됩니다.

Kotlin

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Java

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

반면, 예를 들어 뷰 공개 상태 플래그를 변경한 후 "Hello!" 텍스트가 있는 뷰가 표시되는지 어설션하려는 경우에는 이 코드를 사용해도 됩니다.

뷰 어설션 단순 테스트

이 예에서는 SimpleActivityButtonTextView가 포함되어 있습니다. 버튼을 클릭하면 TextView의 콘텐츠가 "Hello Espresso!"로 변경됩니다.

다음은 Espresso를 사용하여 이를 테스트하는 방법입니다.

버튼 클릭

첫 번째 단계는 버튼을 찾는 데 도움이 되는 속성을 찾는 것입니다. SimpleActivity의 버튼에는 예상대로 고유한 R.id이 있습니다.

Kotlin

onView(withId(R.id.button_simple))

Java

onView(withId(R.id.button_simple));

이제 다음과 같이 클릭을 실행합니다.

Kotlin

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

Java

onView(withId(R.id.button_simple)).perform(click());

TextView 텍스트 확인

확인할 텍스트가 포함된 TextView에도 고유한 R.id가 있습니다.

Kotlin

onView(withId(R.id.text_simple))

Java

onView(withId(R.id.text_simple));

이제 다음과 같이 콘텐츠 텍스트를 확인합니다.

Kotlin

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

Java

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

어댑터 뷰에서 데이터 로드 확인

AdapterView는 어댑터에서 동적으로 데이터를 로드하는 특수한 유형의 위젯입니다. AdapterView의 가장 일반적인 예는 ListView입니다. LinearLayout와 같은 정적 위젯과 달리 AdapterView 하위 요소의 하위 집합만 현재 뷰 계층 구조에 로드할 수 있습니다. 단순 onView() 검색은 현재 로드되지 않은 뷰를 찾지 않습니다.

Espresso는 문제의 어댑터 항목을 먼저 로드할 수 있는 별도의 onData() 진입점을 제공하여 이를 처리합니다. 이 진입점은 문제의 어댑터 항목이나 그 하위 요소에서 작동하기 전에 포커스를 가져옵니다.

경고: AdapterView의 맞춤 구현이 상속 계약, 특히 getItem() API를 위반하면 onData() 메서드에 문제가 발생할 수 있습니다. 이러한 경우 가장 좋은 조치는 애플리케이션 코드를 리팩터링하는 것입니다. 그렇게 할 수 없다면 일치하는 맞춤 AdapterViewProtocol를 구현할 수 있습니다. 자세한 내용은 Espresso에서 제공하는 기본 AdapterViewProtocols 클래스를 참조하세요.

어댑터 뷰 단순 테스트

이 단순 테스트는 onData()를 사용하는 방법을 보여줍니다. SimpleActivity에는 커피 음료의 유형을 나타내는 몇 가지 항목이 있는 Spinner가 포함되어 있습니다. 항목을 선택하면 "One %s a day!"로 변경되는 TextView가 있습니다. 여기서 %s는 선택된 항목을 나타냅니다.

이 테스트의 목표는 Spinner를 열고 특정 항목을 선택한 다음 TextView에 항목이 포함되어 있는지 확인하는 것입니다. Spinner 클래스는 AdapterView를 기반으로 하므로 항목을 일치시키려면 onView() 대신 onData()를 사용하는 것이 좋습니다.

항목 선택 열기

Kotlin

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

Java

onView(withId(R.id.spinner_simple)).perform(click());

항목 선택

항목 선택을 위해 Spinner는 콘텐츠가 있는 ListView를 만듭니다. 이 뷰는 매우 길 수 있으며 요소가 뷰 계층 구조에 기여하지 않을 수 있습니다. onData()를 사용하여 원하는 요소를 뷰 계층 구조에 강제로 적용합니다. Spinner의 항목은 문자열이므로 "Americano" 문자열과 동일한 항목을 일치시키려고 합니다.

Kotlin

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

Java

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

텍스트가 올바른지 확인

Kotlin

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Java

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

디버깅

Espresso는 테스트 실패 시 유용한 디버깅 정보를 제공합니다.

로깅

Espresso는 모든 뷰 작업을 logcat에 로깅합니다. 예:

ViewInteraction: Performing 'single click' action on view with text: Espresso

뷰 계층 구조

Espresso는 onView()가 실패하면 예외 메시지에 뷰 계층 구조를 출력합니다.

  • onView()가 타겟 뷰를 찾지 못하면 NoMatchingViewException이 발생합니다. 예외 문자열에서 뷰 계층 구조를 검사하여 매처가 뷰와 일치하지 않은 이유를 분석할 수 있습니다.
  • onView()가 지정된 매처와 일치하는 뷰를 여러 개 찾으면 AmbiguousViewMatcherException이 발생합니다. 뷰 계층 구조가 출력되고 일치된 모든 뷰가 MATCHES 라벨로 표시됩니다.
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

복잡한 뷰 계층 구조 또는 예상치 못한 위젯 동작을 처리할 때는 설명을 위해 Android 스튜디오의 Hierarchy Viewer를 사용하는 것이 항상 유용합니다.

어댑터 뷰 경고

Espresso는 사용자에게 AdapterView 위젯의 존재에 관해 경고합니다. onView() 작업에서 NoMatchingViewException이 발생하고 AdapterView 위젯이 뷰 계층 구조에 있는 경우 가장 일반적인 해결책은 onData()를 사용하는 것입니다. 예외 메시지에는 어댑터 뷰 목록과 함께 경고가 포함됩니다. 이 정보를 사용하여 타겟 뷰를 로드하는 onData()를 호출할 수 있습니다.

추가 리소스

Android 테스트에서 Espresso를 사용하는 방법에 관한 자세한 내용은 다음 리소스를 참조하세요.

샘플