Espresso 기본 사항

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

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

API 구성요소

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

  • EspressoonView()onData()를 통한 뷰와의 상호작용을 위한 진입점입니다. 또한 반드시 뷰와 연결되지 않아도 되는 API를 노출합니다(예: pressBack()).
  • ViewMatchersMatcher<? super View> 인터페이스를 구현하는 객체의 컬렉션입니다. 이 중 하나 이상을 onView() 메서드에 전달하여 현재 뷰 계층 구조 내에서 뷰를 찾을 수 있습니다.
  • ViewActionsViewInteraction.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()))
    

자바

    // 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))
    

자바

    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!")))
    

자바

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

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

Kotlin

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

자바

    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())
    

자바

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

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

Kotlin

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

자바

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

작업 중인 뷰가 ScrollView(세로 또는 가로) 내부에 있다면 click()typeText()와 같이 뷰가 표시되어야 하는 작업 앞에 scrollTo()를 사용하는 것을 고려하세요. 이렇게 하면 다른 작업을 진행하기 전에 뷰가 표시됩니다.

Kotlin

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

자바

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

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

뷰 어설션 확인

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

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

Kotlin

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

자바

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

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

Kotlin

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

자바

    // 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))
    

자바

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

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

Kotlin

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

자바

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

TextView 텍스트 확인

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

Kotlin

    onView(withId(R.id.text_simple))
    

자바

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

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

Kotlin

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

자바

    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())
    

자바

    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())
    

자바

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

텍스트가 올바른지 확인

Kotlin

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

자바

    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를 사용하는 방법에 관한 자세한 내용은 다음 자료를 참조하세요.

샘플