Espresso 레시피

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

뷰를 옆의 다른 뷰와 일치

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

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

3개 항목 목록 내에 동일한 뷰 요소의 사본 3개를 보여주는 목록 활동

고유하지 않은 뷰는 뷰 옆에 있는 고유한 라벨과 페어링됩니다(예: 통화 버튼 옆에 있는 연락처 이름). 이 경우 hasSibling() 매처를 사용하여 선택 범위를 좁힐 수 있습니다.

Kotlin

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

Java

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

Java

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

Java

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

Java

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

Java

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

Java

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

Java

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
        }
    }
}

Java

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

Java

@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)
        }

    }
}

Java

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에 위임합니다. CustomFailureHandler는 테스트의 setUp() 메서드에서 Espresso에 등록할 수 있습니다.

Kotlin

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

Java

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

Java

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)

Java

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

Java

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

    // ...
}

Java

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() 메서드에 있는 전체 코드 샘플을 살펴보세요.