Espresso 레시피

이 문서에서는 다양한 일반 Espresso 테스트를 설정하는 방법을 설명합니다.

뷰를 옆의 다른 뷰와 일치

레이아웃에 그 자체로 고유하지 않은 특정 뷰가 포함될 수 있습니다. 예를 들어 연락처 테이블의 반복 통화 버튼에 뷰 계층 구조 내의 다른 통화 버튼과 동일한 R.id, 텍스트 및 속성이 있을 수 있습니다.

예를 들어 이 활동에서는 "7"이라는 텍스트가 있는 뷰가 여러 행에서 반복됩니다.

3개 항목이 있는 목록 내에 동일한 뷰 요소가 3개 표시된 목록 활동

고유하지 않은 뷰가 옆에 있는 고유한 일부 라벨과 쌍을 이루는 경우가 흔히 있습니다(예: 통화 버튼 옆에 있는 연락처 이름). 이 경우 다음과 같이 hasSibling() 매처를 사용하여 선택 범위를 좁힐 수 있습니다.

Kotlin

    onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
        .perform(click())
    

자바

    onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
        .perform(click());
    

작업 모음 내부에 있는 뷰 일치

ActionBarTestActivity에는 두 가지의 다른 작업 모음, 즉 일반 작업 모음 및 옵션 메뉴에서 만든 상황별 작업 모음이 있습니다. 두 작업 모음에는 모두 항상 표시되는 항목 하나와 더보기 메뉴에만 표시되는 항목 두 개가 있습니다. 항목을 클릭하면 TextView가 클릭한 항목의 콘텐츠로 변경됩니다.

다음 코드 스니펫에 표시된 것처럼 두 작업 모음 모두에 표시되는 아이콘을 일치시키는 것은 간단합니다.

Kotlin

    fun testClickActionBarItem() {
        // We make sure the contextual action bar is hidden.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click())

        // Click on the icon - we can find it by the r.Id.
        onView(withId(R.id.action_save))
            .perform(click())

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Save")))
    }
    

자바

    public void testClickActionBarItem() {
        // We make sure the contextual action bar is hidden.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click());

        // Click on the icon - we can find it by the r.Id.
        onView(withId(R.id.action_save))
            .perform(click());

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Save")));
    }
    

저장 버튼이 활동 상단의 작업 모음에 있습니다.

상황별 작업 모음의 코드도 동일해 보입니다.

Kotlin

    fun testClickActionModeItem() {
        // Make sure we show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click())

        // Click on the icon.
        onView((withId(R.id.action_lock)))
            .perform(click())

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Lock")))
    }
    

자바

    public void testClickActionModeItem() {
        // Make sure we show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click());

        // Click on the icon.
        onView((withId(R.id.action_lock)))
            .perform(click());

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Lock")));
    }
    

잠금 버튼이 활동 상단의 작업 모음에 있습니다.

일부 기기에는 옵션 메뉴에서 범위를 벗어난 항목을 여는 하드웨어 더보기 메뉴 버튼이 있고 일부 기기에는 일반 더보기 메뉴를 여는 소프트웨어 더보기 메뉴 버튼이 있으므로 일반 작업 모음의 경우 더보기 메뉴의 항목을 클릭하는 것이 약간 더 까다롭습니다. 다행히도 Espresso에서 이 작업을 처리합니다.

일반 작업 모음의 경우 다음과 같습니다.

Kotlin

    fun testActionBarOverflow() {
        // Make sure we hide the contextual action bar.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click())

        // Open the options menu OR open the overflow menu, depending on whether
        // the device has a hardware or software overflow menu button.
        openActionBarOverflowOrOptionsMenu(
                ApplicationProvider.getApplicationContext<Context>())

        // Click the item.
        onView(withText("World"))
            .perform(click())

        // Verify that we have really clicked on the icon by checking
        // the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("World")))
    }
    

자바

    public void testActionBarOverflow() {
        // Make sure we hide the contextual action bar.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click());

        // Open the options menu OR open the overflow menu, depending on whether
        // the device has a hardware or software overflow menu button.
        openActionBarOverflowOrOptionsMenu(
                ApplicationProvider.getApplicationContext());

        // Click the item.
        onView(withText("World"))
            .perform(click());

        // Verify that we have really clicked on the icon by checking
        // the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("World")));
    }
    

더보기 메뉴 버튼이 표시되고 목록이 화면 상단 근처의 작업 모음 아래에 표시됩니다.

하드웨어 더보기 메뉴 버튼이 있는 기기에서는 다음과 같이 표시됩니다.

더보기 메뉴 버튼이 없으며 목록이 화면 하단 근처에 표시됩니다.

상황별 작업 모음의 경우 이 작업이 매우 쉽습니다.

Kotlin

    fun testActionModeOverflow() {
        // Show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click())

        // Open the overflow menu from contextual action mode.
        openContextualActionModeOverflowMenu()

        // Click on the item.
        onView(withText("Key"))
            .perform(click())

        // Verify that we have really clicked on the icon by
        // checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Key")))
        }
    }
    

자바

    public void testActionModeOverflow() {
        // Show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click());

        // Open the overflow menu from contextual action mode.
        openContextualActionModeOverflowMenu();

        // Click on the item.
        onView(withText("Key"))
            .perform(click());

        // Verify that we have really clicked on the icon by
        // checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Key")));
        }
    }
    

더보기 메뉴 버튼이 작업 모음에 표시되고 옵션 목록이 화면 상단 근처의 작업 모음 아래에 표시됩니다.

이러한 샘플의 전체 코드를 확인하려면 GitHub의 ActionBarTest.java 샘플을 보세요.

뷰가 표시되지 않는지 어설션

일련의 작업을 실행한 후 테스트 중인 UI의 상태를 반드시 어설션하는 것이 좋습니다. 가끔 무언가 발생하지 않을 때와 같은 부정적인 경우가 있을 수 있습니다. ViewAssertions.matches()를 사용하여 hamcrest 뷰 매처를 ViewAssertion으로 전환할 수 있다는 점에 유의하세요.

아래 예에서는 isDisplayed() 매처를 가져와서 표준 not() 매처를 사용하여 반전시킵니다.

Kotlin

    import androidx.test.espresso.Espresso.onView
    import androidx.test.espresso.assertion.ViewAssertions.matches
    import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
    import androidx.test.espresso.matcher.ViewMatchers.withId
    import org.hamcrest.Matchers.not

    onView(withId(R.id.bottom_left))
        .check(matches(not(isDisplayed())))
    

자바

    import static androidx.test.espresso.Espresso.onView;
    import static androidx.test.espresso.assertion.ViewAssertions.matches;
    import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
    import static androidx.test.espresso.matcher.ViewMatchers.withId;
    import static org.hamcrest.Matchers.not;

    onView(withId(R.id.bottom_left))
        .check(matches(not(isDisplayed())));
    

위의 접근 방식은 뷰가 여전히 계층 구조의 일부인 경우에 작동합니다. 그렇지 않으면 NoMatchingViewException이 발생하므로 ViewAssertions.doesNotExist()를 사용해야 합니다.

뷰가 없는지 어설션

뷰가 뷰 계층 구조에서 벗어나면(작업으로 인해 다른 활동으로 전환되었을 때 발생할 수 있음) ViewAssertions.doesNotExist()를 사용해야 합니다.

Kotlin

    import androidx.test.espresso.Espresso.onView
    import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
    import androidx.test.espresso.matcher.ViewMatchers.withId

    onView(withId(R.id.bottom_left))
        .check(doesNotExist())
    

자바

    import static androidx.test.espresso.Espresso.onView;
    import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
    import static androidx.test.espresso.matcher.ViewMatchers.withId;

    onView(withId(R.id.bottom_left))
        .check(doesNotExist());
    

데이터 항목이 어댑터에 없는지 어설션

특정 데이터 항목이 AdapterView 내에 없다는 것을 증명하려면 약간 다르게 작업해야 합니다. 관심 있는 AdapterView를 찾아서 이 뷰가 보유하는 데이터를 조사해야 합니다. onData()를 사용할 필요는 없습니다. 대신 onView() 다음과 같이 AdapterView 다른 매처를 사용하여 뷰 내에서 데이터를 작업 할 수 있습니다.

먼저 매처를 찾습니다.

Kotlin

    private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> {
        return object : TypeSafeMatcher<View>() {

            override fun describeTo(description: Description) {
                description.appendText("with class name: ")
                dataMatcher.describeTo(description)
            }

            public override fun matchesSafely(view: View) : Boolean {
                if (view !is AdapterView<*>) {
                    return false
                }

                val adapter = view.adapter
                for (i in 0 until adapter.count) {
                    if (dataMatcher.matches(adapter.getItem(i))) {
                        return true
                    }
                }

                return false
            }
        }
    }
    

자바

    private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
        return new TypeSafeMatcher<View>() {

            @Override
            public void describeTo(Description description) {
                description.appendText("with class name: ");
                dataMatcher.describeTo(description);
            }

            @Override
            public boolean matchesSafely(View view) {
                if (!(view instanceof AdapterView)) {
                    return false;
                }

                @SuppressWarnings("rawtypes")
                Adapter adapter = ((AdapterView) view).getAdapter();
                for (int i = 0; i < adapter.getCount(); i++) {
                    if (dataMatcher.matches(adapter.getItem(i))) {
                        return true;
                    }
                }

                return false;
            }
        };
    }
    

그런 다음, onView()를 사용하여 AdapterView를 찾기만 하면 됩니다.

Kotlin

    fun testDataItemNotInAdapter() {
        onView(withId(R.id.list))
              .check(matches(not(withAdaptedData(withItemContent("item: 168")))))
        }
    }
    

자바

    @SuppressWarnings("unchecked")
    public void testDataItemNotInAdapter() {
        onView(withId(R.id.list))
              .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
        }
    }
    

또한 ID 목록이 포함된 어댑터 뷰에 'item: 168'과 같은 항목이 있는 경우에 실패하는 어설션이 있습니다.

전체 샘플은 GitHub의 AdapterViewTest.java 클래스 내 testDataItemNotInAdapter() 메서드를 참조하세요.

맞춤 실패 핸들러 사용

Espresso의 기본 FailureHandler를 맞춤 실패 핸들러로 대체하면 스크린샷을 찍거나 추가 디버그 정보를 전달하는 등 추가 또는 다른 오류 처리가 가능하게 됩니다.

CustomFailureHandlerTest 예는 맞춤 실패 핸들러를 구현하는 방법을 보여줍니다.

Kotlin

    private class CustomFailureHandler(targetContext: Context) : FailureHandler {
        private val delegate: FailureHandler

        init {
            delegate = DefaultFailureHandler(targetContext)
        }

        override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
            try {
                delegate.handle(error, viewMatcher)
            } catch (e: NoMatchingViewException) {
                throw MySpecialException(e)
            }

        }
    }
    

자바

    private static class CustomFailureHandler implements FailureHandler {
        private final FailureHandler delegate;

        public CustomFailureHandler(Context targetContext) {
            delegate = new DefaultFailureHandler(targetContext);
        }

        @Override
        public void handle(Throwable error, Matcher<View> viewMatcher) {
            try {
                delegate.handle(error, viewMatcher);
            } catch (NoMatchingViewException e) {
                throw new MySpecialException(e);
            }
        }
    }
    

이 실패 핸들러는 NoMatchingViewException 대신 MySpecialException을 발생시키고 다른 모든 실패를 DefaultFailureHandler에 위임합니다. 다음과 같이 테스트의 setUp() 메서드에서 CustomFailureHandler를 Espresso에 등록할 수 있습니다.

Kotlin

    @Throws(Exception::class)
    override fun setUp() {
        super.setUp()
        getActivity()
        setFailureHandler(CustomFailureHandler(
                ApplicationProvider.getApplicationContext<Context>()))
    }
    

자바

    @Override
    public void setUp() throws Exception {
        super.setUp();
        getActivity();
        setFailureHandler(new CustomFailureHandler(
                ApplicationProvider.getApplicationContext()));
    }
    

자세한 내용은 FailureHandler 인터페이스 및 Espresso.setFailureHandler()를 참조하세요.

기본이 아닌 창 타겟팅

Android는 여러 창을 지원합니다. 일반적으로 이러한 다중 창은 사용자 및 앱 개발자에게 표시되지 않지만 자동 완성 창이 검색 위젯의 기본 애플리케이션 창 위에 그려지는 때와 같은 특정 상황에서는 여러 창이 표시됩니다. 작업을 단순화하기 위해 기본적으로 Espresso에서는 휴리스틱을 사용하여 상호작용할 Window를 추측합니다. 대부분 이 휴리스틱으로 충분하지만 드문 경우에 상호작용이 타겟팅해야 하는 창을 지정해야 합니다. 자체 루트 창 매처 또는 Root 매처를 제공하여 창을 지정할 수 있습니다.

Kotlin

    onView(withText("South China Sea"))
        .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
        .perform(click())
    

자바

    onView(withText("South China Sea"))
        .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
        .perform(click());
    

ViewMatchers의 경우와 마찬가지로 사전 규정된 RootMatchers 세트가 제공됩니다. 물론, 언제든지 자체 Matcher 객체를 구현할 수 있습니다.

GitHub의 MultipleWindowTest 샘플을 살펴보세요.

addHeaderView()addFooterView() 메서드를 사용하여 ListViews에 헤더와 바닥글을 추가합니다. Espresso.onData()에서 일치할 데이터 객체를 알게 하려면 미리 설정된 데이터 객체 값을 두 번째 매개변수로 addHeaderView()addFooterView()에 전달해야 합니다. 예를 들면 다음과 같습니다.

Kotlin

    const val FOOTER = "FOOTER"
    ...
    val footerView = layoutInflater.inflate(R.layout.list_item, listView, false)
    footerView.findViewById<TextView>(R.id.item_content).text = "count:"
    footerView.findViewById<TextView>(R.id.item_size).text
            = data.size.toString
    listView.addFooterView(footerView, FOOTER, true)
    

자바

    public static final String FOOTER = "FOOTER";
    ...
    View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
    footerView.findViewById<TextView>(R.id.item_content).setText("count:");
    footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size()));
    listView.addFooterView(footerView, FOOTER, true);
    

그런 다음 바닥글용 매처를 작성할 수 있습니다.

Kotlin

    import org.hamcrest.Matchers.allOf
    import org.hamcrest.Matchers.instanceOf
    import org.hamcrest.Matchers.`is`

    fun isFooter(): Matcher<Any> {
        return allOf(`is`(instanceOf(String::class.java)),
                `is`(LongListActivity.FOOTER))
    }
    

자바

    import static org.hamcrest.Matchers.allOf;
    import static org.hamcrest.Matchers.instanceOf;
    import static org.hamcrest.Matchers.is;

    @SuppressWarnings("unchecked")
    public static Matcher<Object> isFooter() {
        return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
    }
    

테스트에서 뷰를 로드하는 것은 간단합니다.

Kotlin

    import androidx.test.espresso.Espresso.onData
    import androidx.test.espresso.action.ViewActions.click
    import androidx.test.espresso.sample.LongListMatchers.isFooter

    fun testClickFooter() {
        onData(isFooter())
            .perform(click())

        // ...
    }
    

자바

    import static androidx.test.espresso.Espresso.onData;
    import static androidx.test.espresso.action.ViewActions.click;
    import static androidx.test.espresso.sample.LongListMatchers.isFooter;

    public void testClickFooter() {
        onData(isFooter())
            .perform(click());

        // ...
    }
    

GitHub에서 AdapterViewTest.javatestClickFooter() 메서드에 있는 전체 코드 샘플을 살펴보세요.