ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 안드로이드 앱 테스트 기본 - 2 (UI Test예제 포함)
    안드로이드 학습/Android 기술면접 대비 2024. 12. 7. 17:20

    안드로이드에서 UI 테스트는 아래 방법들이 있다

    • Espresso: 구글이 제공하는 UI 테스트 프레임워크.
    • UIAutomator: 시스템 앱을 포함한 더 넓은 범위를 테스트할 때 사용.
    • Compose Test: Jetpack Compose UI를 테스트하는 전용 API.
    • 그 외에 Robotium, Calabash, Robolectric 등등이 있다. 
    • Espresso 공식 샘플 ( Espresso 뿐만 아니라 UIAutomator, JUnit4 등 다양한 샘플 보유)

    1. Espresso

    Espresso는 앱의 UI와 테스트를 자동으로 동기화 해준다.  Espresso는 3가지 기본 component를 갖는다.

    • ViewMatchers - 현재 뷰 계층 구조에서 뷰를 찾을 수 있도록 지원합니다.
    • ViewActions - 뷰에 대한 작업을 수행할 수 있도록 지원합니다.
    • ViewAssertions - 뷰의 상태를 검증할 수 있도록 지원합니다.

    테스트의 기본적인 구조

    onView(ViewMatcher)
        .perfrom(ViewAction)
            .check(ViewAssertion);

     

    1-1. 주요 Espresso 메서드 요약

    1) ViewActions : 액션 (Action) 메서드

    • perform(click()) : 클릭
    • perform(typeText("text")) : 텍스트 입력
    • closeSoftKeyboard() : 소프트 키보드 닫기

    2) ViewMatchers : 매칭 (Matcher) 메서드

    • withId(R.id.viewId) : ID로 찾기
    • withText("text") : 텍스트로 찾기
    • withHint("hint") : 힌트로 찾기

    3) ViewAssertions : 확인 (Assertion) 메서드

    • check(matches(withText("text"))) : 텍스트 확인
    • check(matches(isDisplayed())) : 표시 여부 확인
    • check(matches(not(isEnabled()))) : 비활성화 여부 확인

    1-2) 예제 : 

    build.gradle (:app)

    android {
        defaultConfig {
            testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        }
    }
    
    dependencies {
        androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
        androidTestImplementation("androidx.test.ext:junit:1.1.5")
        androidTestImplementation("androidx.test:rules:1.5.0")
    }

     

    Activity & Fragment

    더보기
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:id="@+id/tv_ui_test_espresso"
            android:layout_width="58dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:text="Espresso Example"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/btn_ui_test_espresso"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="Hello Espresso"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_ui_test_espresso" />
    
        <EditText
            android:id="@+id/et_ui_test_espresso"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:hint="Enter text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btn_ui_test_espresso" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    UiTest

    더보기
    @RunWith(AndroidJUnit4::class)
    class UiTest {
    
        // Activity 테스트 하는 부분
        @get:Rule
        val activityRule = ActivityScenarioRule(UiTestActivity::class.java)
    
        @Before
        fun before() {
            // setUp
        }
    
        @After
        fun after() {
    
        }
    
        @SuppressLint("CheckResult")
        @Test
        fun testActivityButtonClick() {
    
            // EditText
            onView(withId(R.id.et_ui_test_espresso))
                .perform(closeSoftKeyboard())
                .check(matches(isDisplayed()))
    
            // Button
            onView(withId(R.id.btn_ui_test_espresso))
                .perform(click())
                .check(matches(withText("Hello Espresso")))
    
    
            // TextView
            onView(withId(R.id.tv_ui_test_espresso))
                .check(matches(withText("Espresso Example")));
        }
    
    
        @SuppressLint("CheckResult")
        @Test
        fun testFragmentButtonClick() {
    
            val scenario = launchFragmentInContainer<UiTestFragment>()
    
            // EditText
            onView(withId(R.id.et_ui_test_espresso_fragment))
                .perform(closeSoftKeyboard())
                .check(matches(isDisplayed()))
    
            // Button
            onView(withId(R.id.btn_ui_test_espresso_fragment))
                .perform(click())
                .check(matches(withText("Hello Espresso")))
    
    
            // TextView
            onView(withId(R.id.tv_ui_test_espresso_fragment))
                .check(matches(withText("Espresso Example")));
        }
    }

    2. UIAutomator

    UIAutomator 역시 Espresso와 마찬가지로 안드로이드에서 UI를 테스트 할 수 있게 도와주는 라이브러리다.

    2가지의 테스트 용도를 살펴보자면

    • UIAutomator :
      • 블랙박스 테스트,  두개 이상의 앱을 연동하여 테스트할 때 좋음
      • 전체 시스템 및 다른 앱 제어 가능
    • Espresso :
      • 화이트박스 테스트, 한개의 앱만 테스트할 때 좋음
      • UI 상호작용, 화면 전환, 뷰 검증

     UIAutomator는 앱의 구현이 어떻게 되어 있는지 모른다고 한다. 앱 이름으로 앱을 실행 시키고, 어떤 버튼을 누르는지는 버튼 id정보나 텍스트 정보로 가능하게 한다. 그래서 현재 앱 말고도 다른 앱과 연동해서 테스트 가능하다. 

     

    build.gradle

    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' // uiautomator
    androidTestImplementation 'androidx.test:runner:1.6.2'

     

    Before Code : 

    더보기
    private const val BASIC_SAMPLE_PACKAGE = "com.aio.kotlin"
    private const val LAUNCH_TIMEOUT = 5000L
    private const val STRING_TO_BE_TYPED = "UiAutomator"
    
    @RunWith(AndroidJUnit4::class)
    class UiAutomatorTest {
    
        private lateinit var device: UiDevice
    
        @Before
        fun startMainActivityFromHomeScreen() {
            // Initialize UiDevice instance
            device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    
            // Start from the home screen
            device.pressHome()
    
            // Wait for launcher
            val launcherPackage: String = device.launcherPackageName
            assertThat(launcherPackage, notNullValue())
            device.wait(
                Until.hasObject(By.pkg(launcherPackage).depth(0)),
                LAUNCH_TIMEOUT
            )
    
            // Launch the app
            val context = ApplicationProvider.getApplicationContext<Context>()
            val intent = context.packageManager.getLaunchIntentForPackage(
                BASIC_SAMPLE_PACKAGE)?.apply {
                // Clear out any previous instances
                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
            }
            context.startActivity(intent)
    
            // Wait for the app to appear
            device.wait(
                Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
                LAUNCH_TIMEOUT
            )
        }
     }

    테스트에서 UiDevice 메서드를 호출하여 현재 방향 또는 디스플레이 크기와 같은 다양한 속성의 상태를 나타냅니다.

    테스트는 UiDevice 객체를 사용하여 기기 수준 작업을 실행할 수 있습니다. 

     

    Test Code : 

    더보기
    @Test
    fun testLaunchCalculator() {
        device.findObject(By.text("기초").clazz("android.widget.TextView")).click()
    
        val allowButton: UiObject2 = device.wait(
            Until.findObject(By.text("허용").clazz("android.widget.Button")),
            5000 // 최대 5초 기다림
        )
    
        allowButton.click()
    
        device.wait(
            Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(1)),
            LAUNCH_TIMEOUT
        )
    
        // 클릭 후 3초 대기
        Thread.sleep(3000)  // 3초 대기
    }

    Before에서 Activity에 진입했다면 보여지는 Activity 내에서 id나 View의 text 이름으로 action을 수행할 수 있다. 

    @Test
    fun testFindById() {
        // ID로 버튼을 찾기 (예: "com.example.app:id/btnConfirm")
        val button: UiObject2 = device.wait(
            Until.findObject(By.res("com.example.app:id/btnConfirm")),
            5000 // 최대 5초 대기
        )
        
        // 버튼이 존재하면 클릭
        button?.click()
    }
    @Test
    fun testFindByText() {
        // 텍스트로 버튼을 찾기 (예: "확인")
        val button: UiObject2 = device.wait(
            Until.findObject(By.text("확인")),
            5000 // 최대 5초 대기
        )
        
        // 버튼이 존재하면 클릭
        button?.click()
    }

     

    부분 포함된 text로도 가능하다.

    @Test
    fun testFindByTextContains() {
        // 텍스트 일부로 버튼을 찾기 (예: "확인"이 포함된 텍스트)
        val button: UiObject2 = device.wait(
            Until.findObject(By.textContains("확인")),
            5000 // 최대 5초 대기
        )
        
        // 버튼이 존재하면 클릭
        button?.click()
    }

     

    객체가 실행 가능한 메소드

    테스트에서 UiObject2 객체를 가져오면 다음에서 메서드를 호출할 수 있습니다. UI 구성요소에서 사용자 상호작용을 실행하는 UiObject2 클래스 해당 객체로 표시됩니다. 다음과 같은 작업을 지정할 수 있습니다.

    • click() : UI 요소의 표시된 경계 중심을 클릭합니다.
    • drag() : 이 객체를 임의의 좌표로 드래그합니다.
    • setText(): 필드 콘텐츠를 나타냅니다. 반대로 clear() 메서드는 기존 텍스트를 지웁니다. 입력 가능한 입력란에 값을 입력합니다.
    • swipe() : 지정된 방향으로 스와이프 작업을 실행합니다.
    • scrollUntil(): 지정된 방향으로 스크롤 작업을 실행합니다. Condition 또는 EventCondition가 충족될 때까지만 유지됩니다.
Designed by Tistory.