단일 앱의 UI 테스트

단일 앱 내의 사용자 상호작용을 테스트하면 사용자가 앱과 상호작용할 때 예기치 않은 결과가 발생하거나 불만족스러운 경험을 하지 않도록 할 수 있습니다. 앱의 사용자 인터페이스(UI)가 올바르게 작동하는지 확인해야 하는 경우 UI 테스트를 만드는 습관을 들여야 합니다.

AndroidX 테스트에서 제공하는 Espresso 테스트 프레임워크는 단일 타겟 앱 내의 사용자 상호작용을 시뮬레이션하는 UI 테스트를 작성하기 위한 API를 제공합니다. Espresso 테스트는 Android 2.3.3(API 레벨 10) 이상을 실행하는 기기에서 실행될 수 있습니다. Espresso 사용의 주요 이점은 테스트 중인 앱의 UI와 테스트 작업을 자동으로 동기화한다는 것입니다. Espresso는 기본 스레드가 유휴 상태인 시점을 감지하므로 적절한 시간에 테스트 명령어를 실행하여 테스트 신뢰성을 향상할 수 있습니다. 또한 이 기능을 사용하면 Thread.sleep()과 같은 타이밍 해결 방법을 테스트 코드에 추가하지 않아도 됩니다.

Espresso 테스트 프레임워크는 계측 기반 API이며 AndroidJUnitRunner 테스트 실행기와 함께 작동합니다.

Espresso 설정

Espresso를 사용하여 UI 테스트를 빌드하기 전에 Espresso 라이브러리의 종속성 참조를 설정해야 합니다.

    dependencies {
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    }
    

테스트 기기에서 애니메이션 사용 중지 - 테스트 기기에서 시스템 애니메이션을 켜두면 예기치 않은 결과가 발생하거나 테스트가 실패할 수 있습니다. Settings에서 Developer options를 열고 다음 옵션을 모두 사용 중지하여 애니메이션을 사용 중지하세요.

  • 창 애니메이션 배율
  • 전환 애니메이션 배율
  • 애니메이터 길이 배율

핵심 API에서 제공하는 기능 이외의 Espresso 기능을 사용하도록 프로젝트를 설정하려면 Espresso와 관련된 가이드를 참조하세요.

Espresso 테스트 클래스 만들기

Espresso 테스트를 만들려면 다음 프로그래밍 모델을 따르세요.

  1. onView() 메서드 또는 AdapterView 컨트롤을 위한 onData() 메서드를 호출하여 Activity에서 테스트할 UI 구성요소를 찾습니다(예: 앱의 로그인 버튼).
  2. ViewInteraction.perform() 또는 DataInteraction.perform() 메서드를 호출하고 사용자 작업을 전달하여 해당하는 UI 구성요소에서 실행할 특정 사용자 상호작용을 시뮬레이션합니다(예: 로그인 버튼 클릭). 동일한 UI 구성요소에서 여러 작업을 시퀀싱하려면 메서드 인수에서 쉼표로 구분된 목록을 사용하여 작업을 연결합니다.
  3. 필요에 따라 위의 단계를 반복하여 타겟 앱의 여러 활동에 걸친 사용자 플로우를 시뮬레이션합니다.
  4. 이러한 사용자 상호작용이 이뤄진 후 ViewAssertions 메서드를 사용하여 UI에 예상 상태 또는 동작이 반영되었는지 확인합니다.

아래 섹션에서 이러한 단계를 더 자세히 다룹니다.

다음 코드 스니펫은 테스트 클래스가 이 기본 워크플로를 호출하는 방법을 보여줍니다.

Kotlin

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

자바

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

ActivityTestRule과 함께 Espresso 사용

다음 섹션에서는 JUnit 4 스타일의 새 Espresso 테스트를 만들고 ActivityTestRule을 사용하여 작성해야 하는 상용구 코드의 양을 줄이는 방법을 설명합니다. ActivityTestRule을 사용하면 테스트 프레임워크가 @Test 주석이 달린 각 테스트 메서드 및 @Before 주석이 달린 모든 메서드 이전에 테스트 중인 활동을 시작합니다. 프레임워크는 테스트가 완료되고 @After 주석이 달린 모든 메서드가 실행된 후 활동 종료를 처리합니다.

Kotlin

    package com.example.android.testing.espresso.BasicSample

    import org.junit.Before
    import org.junit.Rule
    import org.junit.Test
    import org.junit.runner.RunWith

    import androidx.test.rule.ActivityTestRule
    import androidx.test.runner.AndroidJUnit4

    @RunWith(AndroidJUnit4::class)
    @LargeTest
    class ChangeTextBehaviorTest {

        private lateinit var stringToBetyped: String

        @get:Rule
        var activityRule: ActivityTestRule<MainActivity>
                = ActivityTestRule(MainActivity::class.java)

        @Before
        fun initValidString() {
            // Specify a valid string.
            stringToBetyped = "Espresso"
        }

        @Test
        fun changeText_sameActivity() {
            // Type text and then press the button.
            onView(withId(R.id.editTextUserInput))
                    .perform(typeText(stringToBetyped), closeSoftKeyboard())
            onView(withId(R.id.changeTextBt)).perform(click())

            // Check that the text was changed.
            onView(withId(R.id.textToBeChanged))
                    .check(matches(withText(stringToBetyped)))
        }
    }
    

자바

    package com.example.android.testing.espresso.BasicSample;

    import org.junit.Before;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.runner.RunWith;

    import androidx.test.rule.ActivityTestRule;
    import androidx.test.runner.AndroidJUnit4;

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class ChangeTextBehaviorTest {

        private String stringToBetyped;

        @Rule
        public ActivityTestRule<MainActivity> activityRule
                = new ActivityTestRule<>(MainActivity.class);

        @Before
        public void initValidString() {
            // Specify a valid string.
            stringToBetyped = "Espresso";
        }

        @Test
        public void changeText_sameActivity() {
            // Type text and then press the button.
            onView(withId(R.id.editTextUserInput))
                    .perform(typeText(stringToBetyped), closeSoftKeyboard());
            onView(withId(R.id.changeTextBt)).perform(click());

            // Check that the text was changed.
            onView(withId(R.id.textToBeChanged))
                    .check(matches(withText(stringToBetyped)));
        }
    }
    

UI 구성요소에 액세스

Espresso가 테스트 중인 앱과 상호작용하려면 먼저 UI 구성요소 또는 를 지정해야 합니다. Espresso는 Hamcrest 매처를 사용하여 앱에 뷰 및 어댑터를 지정할 수 있도록 지원합니다.

뷰를 찾으려면 onView() 메서드를 호출하고 타겟팅하는 뷰를 지정하는 뷰 매처를 전달하세요. 이 내용은 뷰 매처 지정에 자세히 설명되어 있습니다. onView() 메서드는 테스트가 뷰와 상호작용할 수 있도록 하는 ViewInteraction 개체를 반환합니다. 그러나 RecyclerView 레이아웃에서 뷰를 찾으려는 경우 onView() 메서드 호출이 작동하지 않을 수 있습니다. 이 경우에는 대신 AdapterView에서 뷰 찾기의 안내를 따르세요.

참고: onView() 메서드는 지정한 뷰가 유효한지 여부를 확인하지 않습니다. 대신 Espresso는 제공된 매처를 사용하여 현재 뷰 계층 구조만 검색합니다. 일치하는 항목이 없으면 이 메서드가 NoMatchingViewException을 발생시킵니다.

다음 코드 스니펫은 EditText 필드에 액세스하여 텍스트 문자열을 입력하고 가상 키보드를 닫은 다음 버튼 클릭을 실행하는 테스트를 작성하는 방법을 보여줍니다.

Kotlin

    fun testChangeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard())
        onView(withId(R.id.changeTextButton)).perform(click())

        // Check that the text was changed.
        ...
    }
    

자바

    public void testChangeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
        onView(withId(R.id.changeTextButton)).perform(click());

        // Check that the text was changed.
        ...
    }
    

뷰 매처 지정

다음 접근 방식을 사용하여 뷰 매처를 지정할 수 있습니다.

  • ViewMatchers 클래스에서 메서드를 호출합니다. 예를 들어 뷰에 표시되는 텍스트 문자열을 찾아서 뷰를 찾으려면 다음과 같은 메서드를 호출하면 됩니다.

    Kotlin

        onView(withText("Sign-in"))
        

    자바

        onView(withText("Sign-in"));
        

    마찬가지로 다음 예와 같이 withId()를 호출하고 뷰의 리소스 ID(R.id)를 제공할 수 있습니다.

    Kotlin

        onView(withId(R.id.button_signin))
        

    자바

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

    Android 리소스 ID가 반드시 고유한 것은 아닙니다. 테스트가 둘 이상의 뷰에서 사용된 리소스 ID에 일치시키려고 하면 Espresso에서 AmbiguousViewMatcherException을 발생시킵니다.

  • Hamcrest Matchers 클래스를 사용합니다. allOf() 메서드를 사용하여 containsString()instanceOf()와 같은 여러 매처를 결합할 수 있습니다. 이 접근 방식을 사용하면 다음 예와 같이 일치 결과를 더 좁은 범위로 필터링할 수 있습니다.

    Kotlin

        onView(allOf(withId(R.id.button_signin), withText("Sign-in")))
        

    자바

        onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
        

    다음 예와 같이 not 키워드를 사용하여 매처에 해당하지 않는 뷰를 필터링할 수 있습니다.

    Kotlin

        onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))))
        

    자바

        onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));
        

    테스트에서 이러한 메서드를 사용하려면 org.hamcrest.Matchers 패키지를 가져오세요. Hamcrest 일치에 관해 자세히 알아보려면 Hamcrest 사이트를 참조하세요.

Espresso 테스트의 성능을 향상하려면 타겟 뷰를 찾는 데 필요한 최소 일치 정보를 지정하세요. 예를 들어 설명 텍스트로 뷰를 고유하게 식별할 수 있는 경우 TextView 인스턴스에서 뷰를 할당할 수 있도록 지정할 필요가 없습니다.

AdapterView에서 뷰 찾기

AdapterView 위젯에서는 런타임에 뷰가 하위 뷰로 동적으로 채워집니다. 테스트할 타겟 뷰가 AdapterView 내에 있는 경우(예: ListView, GridView 또는 Spinner) 뷰 중 일부만 현재 뷰 계층 구조에서 로드될 수 있기 때문에 onView() 메서드가 작동하지 않을 수도 있습니다.

대신 onData() 메서드를 호출하여 타겟 뷰 요소에 액세스하기 위한 DataInteraction 개체를 가져오세요. Espresso에서 타겟 뷰 요소를 현재 뷰 계층 구조로 로드하는 작업을 처리합니다. 또한 Espresso가 타겟 요소로 스크롤하고 요소에 포커스를 맞춥니다.

참고: onData() 메서드는 지정한 항목이 뷰와 일치하는지 여부를 확인하지 않습니다. Espresso는 현재 뷰 계층 구조만 검색합니다. 일치하는 항목이 없으면 이 메서드가 NoMatchingViewException을 발생시킵니다.

다음 코드 스니펫은 onData() 메서드를 Hamcrest 일치와 함께 사용하여 목록에서 주어진 문자열을 포함하는 특정 행을 검색하는 방법을 보여줍니다. 이 예에서는 LongListActivity 클래스에 SimpleAdapter를 통해 노출된 문자열 목록이 포함되어 있습니다.

Kotlin

    onData(allOf(`is`(instanceOf(Map::class.java)),
            hasEntry(equalTo(LongListActivity.ROW_TEXT),
            `is`("test input"))))
    

자바

    onData(allOf(is(instanceOf(Map.class)),
            hasEntry(equalTo(LongListActivity.ROW_TEXT), is("test input"))));
    

작업 실행

UI 구성요소에서 사용자 상호작용을 시뮬레이션하려면 ViewInteraction.perform() 또는 DataInteraction.perform() 메서드를 호출하세요. 하나 이상의 ViewAction 개체를 인수로 전달해야 합니다. Espresso가 주어진 순서에 따라 각 동작을 순차적으로 실행하고 기본 스레드에서 동작을 실행합니다.

ViewActions 클래스는 일반 작업을 지정하기 위한 도우미 메서드 목록을 제공합니다. 개별 ViewAction 개체를 만들고 구성하는 대신 이러한 메서드를 편리한 바로가기로 사용할 수 있습니다. 다음과 같은 작업을 지정할 수 있습니다.

타겟 뷰가 ScrollView 내에 있는 경우 다른 작업을 진행하기 전에 먼저 ViewActions.scrollTo() 작업을 실행하여 뷰를 화면에 표시하세요. 뷰가 이미 표시되어 있으면 ViewActions.scrollTo() 작업이 적용되지 않습니다.

Espresso Intents를 사용하여 격리된 상태로 활동 테스트

Espresso Intents를 통해 앱에서 전송된 인텐트를 유효성 검사하고 스텁할 수 있습니다. Espresso Intents를 사용하면 발신 인텐트를 가로채고 결과를 스텁한 후 테스트 중인 구성요소에 다시 전송하여 앱, 활동 또는 서비스를 격리된 상태로 테스트할 수 있습니다.

Espresso Intents를 사용하여 테스트를 시작하려면 다음 줄을 앱의 build.gradle 파일에 추가해야 합니다.

    dependencies {
      androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
    }
    

인텐트를 테스트하려면 ActivityTestRule 클래스와 매우 유사한 IntentsTestRule 클래스의 인스턴스를 만들어야 합니다. IntentsTestRule 클래스는 각 테스트 전에 Espresso Intents를 초기화하고 호스트 활동을 종료하며 각 테스트 후에 Espresso Intents를 해제합니다.

다음 코드 스니펫에 표시된 테스트 클래스는 명시적 인텐트와 관련된 단순 테스트를 제공합니다. 이 클래스는 첫 앱 빌드 가이드에서 만든 활동 및 인텐트를 테스트합니다.

Kotlin

    private const val MESSAGE = "This is a test"
    private const val PACKAGE_NAME = "com.example.myfirstapp"

    @RunWith(AndroidJUnit4::class)
    class SimpleIntentTest {

        /* Instantiate an IntentsTestRule object. */
        @get:Rule
        var intentsRule: IntentsTestRule<MainActivity> = IntentsTestRule(MainActivity::class.java)

        @Test
        fun verifyMessageSentToMessageActivity() {

            // Types a message into a EditText element.
            onView(withId(R.id.edit_message))
                    .perform(typeText(MESSAGE), closeSoftKeyboard())

            // Clicks a button to send the message to another
            // activity through an explicit intent.
            onView(withId(R.id.send_message)).perform(click())

            // Verifies that the DisplayMessageActivity received an intent
            // with the correct package name and message.
            intended(allOf(
                    hasComponent(hasShortClassName(".DisplayMessageActivity")),
                    toPackage(PACKAGE_NAME),
                    hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)))

        }
    }
    

자바

    @Large
    @RunWith(AndroidJUnit4.class)
    public class SimpleIntentTest {

        private static final String MESSAGE = "This is a test";
        private static final String PACKAGE_NAME = "com.example.myfirstapp";

        /* Instantiate an IntentsTestRule object. */
        @Rule
        public IntentsTestRule<MainActivity> intentsRule =
                new IntentsTestRule<>(MainActivity.class);

        @Test
        public void verifyMessageSentToMessageActivity() {

            // Types a message into a EditText element.
            onView(withId(R.id.edit_message))
                    .perform(typeText(MESSAGE), closeSoftKeyboard());

            // Clicks a button to send the message to another
            // activity through an explicit intent.
            onView(withId(R.id.send_message)).perform(click());

            // Verifies that the DisplayMessageActivity received an intent
            // with the correct package name and message.
            intended(allOf(
                    hasComponent(hasShortClassName(".DisplayMessageActivity")),
                    toPackage(PACKAGE_NAME),
                    hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)));

        }
    }
    

Espresso Intents에 관한 자세한 내용은 AndroidX 테스트 사이트에 있는 Espresso Intents 문서를 참조하세요. IntentsBasicSampleIntentsAdvancedSample 코드 샘플을 다운로드할 수도 있습니다.

Espresso Web을 사용하여 WebView 테스트

Espresso Web을 사용하여 활동 내에 포함된 WebView 구성요소를 테스트할 수 있습니다. Espresso Web은 WebDriver API를 사용하여 WebView의 동작을 검사하고 제어합니다.

Espresso Web을 사용하여 테스트를 시작하려면 다음 줄을 앱의 build.gradle 파일에 추가해야 합니다.

    dependencies {
      androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
    }
    

Espresso Web을 사용하여 테스트를 만드는 경우 ActivityTestRule 개체를 인스턴스화하여 활동을 테스트할 때 WebView에서 자바스크립트를 사용 설정해야 합니다. 테스트에서 WebView에 표시된 HTML 요소를 선택하고 텍스트 상자에 텍스트를 입력한 다음 버튼을 클릭하여 사용자 상호작용을 시뮬레이션할 수 있습니다. 작업이 완료된 후 웹페이지의 결과가 예상한 결과와 일치하는지 확인할 수 있습니다.

다음 코드 스니펫에서는 이 클래스가 테스트 중인 활동에서 ID 값이 'webview'인 WebView 구성요소를 테스트합니다. typeTextInInput_clickButton_SubmitsForm() 테스트는 웹페이지에서 <input> 요소를 선택하고 일부 텍스트를 입력한 후 다른 요소에 표시되는 텍스트를 확인합니다.

Kotlin

    private const val MACCHIATO = "Macchiato"
    private const val DOPPIO = "Doppio"

    @LargeTest
    @RunWith(AndroidJUnit4::class)
    class WebViewActivityTest {

        @get:Rule
        val activityRule = object : ActivityTestRule<WebViewActivity>(
                WebViewActivity::class.java,
                false,      /* Initial touch mode */
                false       /* launch activity */
        ) {
            override fun afterActivityLaunched() {
                // Enable JavaScript.
                onWebView().forceJavascriptEnabled()
            }
        }

        @Test
        fun typeTextInInput_clickButton_SubmitsForm() {
            // Lazily launch the Activity with a custom start Intent per test
            activityRule.launchActivity(withWebFormIntent())

            // Selects the WebView in your layout.
            // If you have multiple WebViews you can also use a
            // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
            onWebView()
                    // Find the input element by ID
                    .withElement(findElement(Locator.ID, "text_input"))
                    // Clear previous input
                    .perform(clearElement())
                    // Enter text into the input element
                    .perform(DriverAtoms.webKeys(MACCHIATO))
                    // Find the submit button
                    .withElement(findElement(Locator.ID, "submitBtn"))
                    // Simulate a click via JavaScript
                    .perform(webClick())
                    // Find the response element by ID
                    .withElement(findElement(Locator.ID, "response"))
                    // Verify that the response page contains the entered text
                    .check(webMatches(getText(), containsString(MACCHIATO)))
        }
    }
    

자바

    @LargeTest
    @RunWith(AndroidJUnit4.class)
    public class WebViewActivityTest {

        private static final String MACCHIATO = "Macchiato";
        private static final String DOPPIO = "Doppio";

        @Rule
        public ActivityTestRule<WebViewActivity> activityRule =
            new ActivityTestRule<WebViewActivity>(WebViewActivity.class,
                false /* Initial touch mode */, false /*  launch activity */) {

            @Override
            protected void afterActivityLaunched() {
                // Enable JavaScript.
                onWebView().forceJavascriptEnabled();
            }
        }

        @Test
        public void typeTextInInput_clickButton_SubmitsForm() {
           // Lazily launch the Activity with a custom start Intent per test
           activityRule.launchActivity(withWebFormIntent());

           // Selects the WebView in your layout.
           // If you have multiple WebViews you can also use a
           // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
           onWebView()
               // Find the input element by ID
               .withElement(findElement(Locator.ID, "text_input"))
               // Clear previous input
               .perform(clearElement())
               // Enter text into the input element
               .perform(DriverAtoms.webKeys(MACCHIATO))
               // Find the submit button
               .withElement(findElement(Locator.ID, "submitBtn"))
               // Simulate a click via JavaScript
               .perform(webClick())
               // Find the response element by ID
               .withElement(findElement(Locator.ID, "response"))
               // Verify that the response page contains the entered text
               .check(webMatches(getText(), containsString(MACCHIATO)));
        }
    }
    

Espresso Web에 관한 자세한 내용은 AndroidX 테스트 사이트에 있는 Espresso Web 문서를 참조하세요. 이 코드 스니펫을 Espresso Web 코드 샘플의 일부로 다운로드할 수도 있습니다.

결과 확인

UI의 뷰가 일부 예상 상태와 일치하는지 확인하려면 ViewInteraction.check() 또는 DataInteraction.check() 메서드를 호출하세요. ViewAssertion 개체를 인수로 전달해야 합니다. 어설션에 실패하면 Espresso에서 AssertionFailedError를 발생시킵니다.

ViewAssertions 클래스는 일반 어설션을 지정하기 위한 도우미 메서드 목록을 제공합니다. 사용할 수 있는 어설션은 다음과 같습니다.

  • doesNotExist: 현재 뷰 계층 구조에 지정된 기준과 일치하는 뷰가 없는지 어설션합니다.
  • matches: 지정된 뷰가 현재 뷰 계층 구조에 있으며 뷰의 상태가 주어진 Hamcrest 매처와 일치하는지 어설션합니다.
  • selectedDescendentsMatch: 상위 뷰에 지정된 하위 뷰가 존재하고 상태가 주어진 Hamcrest 매처와 일치하는지 어설션합니다.

다음 코드 스니펫은 UI에 표시되는 텍스트가 이전에 EditText 필드에 입력한 텍스트와 동일한 값인지 확인하는 방법을 보여줍니다.

Kotlin

    fun testChangeText_sameActivity() {
        // Type text and then press the button.
        ...

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(STRING_TO_BE_TYPED)))
    }
    

자바

    public void testChangeText_sameActivity() {
        // Type text and then press the button.
        ...

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(STRING_TO_BE_TYPED)));
    }
    

기기 또는 에뮬레이터에서 Espresso 테스트 실행

Android 스튜디오 또는 명령줄에서 Espresso 테스트를 실행할 수 있습니다. 프로젝트에서 AndroidJUnitRunner를 기본 계측 실행기로 지정해야 합니다.

Espresso 테스트를 실행하려면 테스트 시작하기에 설명된 계측 테스트 실행 단계를 따르세요.

Espresso API 참조도 참조해야 합니다.

참고 자료

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

샘플

Codelab