단일 앱의 UI 테스트

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

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

Espresso 테스트 프레임워크는 계측 기반 API이며 AndroidJUnitRunner 테스트 실행기와 호환됩니다.

Espresso 설정

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

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

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

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

핵심 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"))));
    

작업 실행

ViewInteraction.perform() 또는 DataInteraction.perform() 메서드를 호출하여 UI 구성요소에서 사용자 상호작용을 시뮬레이션합니다. 하나 이상의 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 코드 샘플의 일부로 다운로드할 수도 있습니다.

결과 확인

ViewInteraction.check() 또는 DataInteraction.check() 메서드를 호출하여 UI의 뷰가 예상 상태와 일치하는지 어설션합니다. 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를 사용하는 방법에 관한 자세한 내용은 다음 자료를 참조하세요.

샘플

코드랩