관리 메뉴

나만을 위한 블로그

[Android] Espresso Web이란? 웹뷰 UI 테스트 예제 본문

Android

[Android] Espresso Web이란? 웹뷰 UI 테스트 예제

참깨빵위에참깨빵 2023. 4. 15. 21:39
728x90
반응형

안드로이드에서 UI 테스트를 위해 만든 라이브러리는 Espresso가 있다. 그러나 이것은 네이티브 화면을 대상으로 만들어진 라이브러리라서, 웹뷰를 중심으로 하는 하이브리드 앱에선 사용할 수 없다. 이 경우 Espresso Web 라이브러리를 써서 웹뷰 대상으로 UI 테스트를 진행할 수 있다.

디벨로퍼에서 말하는 Espresso Web 라이브러리는 아래와 같다.

 

https://developer.android.com/training/testing/espresso/web?hl=ko 

 

Espresso Web  |  Android 개발자  |  Android Developers

Espresso Web 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Espresso-Web은 Android WebView UI 구성요소를 사용하기 위한 진입점입니다. Espresso-Web은 인기 WebDriver API의

developer.android.com

Espresso Web은...(중략)...WebDriver API의 Atom을 재사용해서 웹뷰 동작을 검사하고 제어한다. 하이브리드 앱, 특히 앱의 네이티브 UI 컴포넌트와 웹뷰 UI 컴포넌트의 통합을 테스트하려면 Espresso Web을 사용한다. Espresso Web API를 다른 Espresso API와 함께 써서 웹뷰 객체 내부의 웹 요소와 상호작용할 수 있다. 웹뷰와 앱의 네이티브 컴포넌트 간 상호작용이 아니라 웹뷰 자체만 테스트해야 한다면 WebDriver 같은 프레임워크를 써서 일반 웹 테스트를 작성하라
WebDriver 프레임워크는 Atom을 써서 프로그래매틱 방식으로 웹 요소를 찾고 조작한다. Atom은 WebDriver에서 브라우저 조작을 허용하는 데 쓰인다. Atom은 개념적으로 UI에서 작업을 실행하는 독립 단위인 ViewAction과 유사하다. findElement() 및 getElement() 같은 정의된 메서드 목록을 통해 Atom을 노출해 사용자 관점에서 브라우저를 구동한다. 그러나 WebDriver를 직접 사용하면 Atom이 적절하게 조정돼야 하므로 매우 상세한 로직이 필요하다...(중략)

 

이제 Espresso Web을 써서 웹뷰를 테스트하는 예제를 확인한다.

사용하려면 앱 gradle에 라이브러리를 추가한다. 오늘 기준으로 최신 버전은 3.5.1이다.

 

androidTestImplementation "androidx.test.espresso:espresso-web:3.5.1"

 

이제 테스트 파일을 작성한다. UI 테스트 파일은 패키지 중 "androidTest"라는 패키지에 만들어야 한다. 먼저 공통으로 사용되는 함수들을 모아 둘 Base 파일을 만든다.

 

import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.*
import androidx.test.espresso.web.webdriver.Locator
import org.hamcrest.Matcher

open class BaseWebViewTest {
    fun clickWebViewElement(cssSelector: String, waitTime: Long = 1L) {
        onWebView()
            .withElement(findElement(Locator.CSS_SELECTOR, cssSelector))
            .perform(webClick())

        wait(waitTime)
    }

    fun inputTextToWebViewElement(cssSelector: String, waitTime: Long = 1L, text: String) {
        onWebView()
            .withElement(findElement(Locator.CSS_SELECTOR, cssSelector))
            .perform(webKeys(text))

        wait(waitTime)
    }
    
    private fun wait(millis: Long): ViewInteraction = onView(isRoot()).perform(waitFor(millis))

    private fun waitFor(millis: Long): ViewAction = object : ViewAction {
        override fun getDescription(): String = "${millis}초 기다림"

        override fun getConstraints(): Matcher<View> = isRoot()

        override fun perform(uiController: UiController, view: View?) =
            uiController.loopMainThreadForAtLeast(millis)
    }
}

 

getDescription()는 테스트 결과 보고서에서 ViewAction이 수행한 작업을 설명하기 위해 사용된다. ViewAction을 구현하면 기본적으로 재정의해야 하는 3가지 메서드기 때문에 난 임의로 몇 밀리초를 기다린다고 썼지만, 보고서가 필요없는 사람이라면 공백을 리턴시키도록 해도 될 것이다.

내가 편의상 만든 함수기 때문에 간단하게 코드를 확인하고 간다. 코드만 필요하다면 스크롤 내려서 다음 코드들을 복붙하면 된다.

 

clickWebViewElement()는 이름에서 유추할 수 있겠지만 웹뷰 안의 특정 요소를 클릭하기 위해 만든 함수다.

onWebView()를 통해 웹뷰에서 어떤 작업을 수행할 거라고 설정한 뒤 withElement()로 요소를 찾는다. 이 안에서 findElement() 안에 Locator와 cssSelector를 사용해 웹뷰 안에서 내가 원하는 요소의 css selector를 가져온다.

css를 모르는데 어떻게 가져오냐는 생각은 안 해도 된다. 안드로이드에서 모두 준비해 둔 게 있다. 이 부분은 예제 코드와 같이 봐야 하기 때문에 지금은 위와 같이 정의해둔다.

그 다음 perform(webClick())을 통해 내가 원하는 웹뷰 요소를 클릭한다. 그리고 waitTime()이 호출된다. waitTime 값을 따로 설정해주지 않았다면 0.001초 기다린 다음 곧바로 다음 액션이 실행될 것이다. 밀리초기 때문에 1초를 기다려야 한다면 1000L을 넣어주면 된다.

그 밑의 inputTextToWebViewElement()도 비슷하기 때문에 넘어간다.

 

이제 액티비티와 XML을 간단하게 작성한다.

 

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".espressoWeb.EspressoWebTestActivity">

        <WebView
            android:id="@+id/webView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebViewClient
import com.example.kotlinprac.BaseActivity
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ActivityEspressoWebTestBinding

class EspressoWebTestActivity :
    BaseActivity<ActivityEspressoWebTestBinding>(R.layout.activity_espresso_web_test) {

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        bind {
            webView.apply {
                settings.javaScriptEnabled = true
                webViewClient = object : WebViewClient() {}
                webChromeClient = object : WebChromeClient() {}
                loadUrl("https://www.aladin.co.kr/home/welcome.aspx")
            }
        }
    }
}

 

테스트에 사용할 url은 인터넷 서점인 알라딘이다. 데이터 바인딩과 웹뷰 설정 등은 알아서 세팅한다.

이제 테스트 코드를 보자.

 

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.kotlinprac.BaseWebViewTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class EspressoWebTestActivityTest: BaseWebViewTest() {
    @get:Rule
    val activityScenarioRule = ActivityScenarioRule(EspressoWebTestActivity::class.java)

    @Test
    fun `왼쪽_상단_햄버거_메뉴_클릭_테스트`() {
        clickWebViewElement(
            cssSelector = "#welcom_wrap > div.aladin-gnb.main > div.drawer > a > img",
            waitTime = 2000L
        )
    }

    @Test
    fun `글자_입력_테스트`() {
        clickWebViewElement(
            cssSelector = "#SearchWordBanner",
        )

        inputTextToWebViewElement(
            cssSelector = "#SearchWord",
            text = "컴퓨터\n"
        )

        clickWebViewElement(
            cssSelector = "#welcom_wrap > div.aladin-gnb.main > div.sch-wrap > div > div > a > img",
            waitTime = 3000L
        )
    }
}

 

테스트 케이스(=함수)의 이름은 본인 또는 팀의 컨벤션에 맞춰서 알아서 수정한다.

코드를 보면 cssSelector의 값으로 무슨 문자열들이 길게 써져 있다. 저것이 css selector다.

저것을 확인하려면 먼저 앱을 실행해서 화면을 띄워놓는다. 그리고 크롬 주소창에 "chrome://inspect"라고 입력한다. 그럼 이런 화면이 표시될 것이다. 표시되지 않는다면 홈 버튼을 눌러서 앱을 잠깐 숨겼다가 다시 띄워놓고 조금 기다리거나 새로고침을 하면 표시된다.

 

 

에뮬레이터로 실행했기 때문에 SDK built 어쩌고라고 표시되는데, 하단의 inspect를 누르면 이 화면이 표시될 것이다.

 

 

알라딘 메인 화면의 html 소스코드다. 이제 저기서 왼쪽 위의 햄버거 버튼의 css selector를 가져온다.

빨간색으로 표시해둔 버튼을 누르면 파란색으로 변하는데, 이 상태로 마우스를 웹뷰 화면 위로 갖다대면 여러 툴팁 박스들이 표시될 것이다. 이 상태로 햄버거 버튼을 클릭하면 오른쪽 소스코드가 햄버거 버튼의 소스코드를 표시할 것이다.

 

 

그 후 저 소스코드를 우클릭해서 Copy -> Copy selector 순으로 클릭한다. 그러면 저 햄버거 버튼의 css selector가 복사된다.

이 과정을 통해 얻어온 햄버거 버튼의 css selector가 예제 코드 첫 번째 함수 안에 있는 문자열이다. 그리고 햄버거 버튼을 클릭한 다음 2초 기다린다. 기다리는 이유는 웹뷰가 모두 로드될 때까지 충분한 시간 동안 기다려야 이후 클릭 등을 할 때 라이브러리가 웹뷰 요소를 찾지 못해 null을 뿜으면서 테스트가 실패하는 걸 방지할 수 있기 때문이다.

 

코드들을 모두 복붙하고 실행한다면 아래와 같이 작동할 것이다.

 

 

내가 뭘 하지 않아도 코드를 짠 대로 에뮬레이터 안의 웹뷰가 제멋대로 움직이고 종료되는 걸 볼 수 있다. 테스트가 모두 성공하면 왼쪽 하단에 녹색 툴팁박스가 표시되면서 테스트가 통과됐다고 표시된다.

이렇게 웹뷰를 대상으로 UI 테스트를 진행하면 된다. 화면에 표시되지 않는 맨 밑의 아이콘도 코드에서 스크롤 처리 없이 클릭하라고만 짜 두면 알아서 클릭되니 편하다. 단점이라면 화면을 테스트하기 때문에 앱을 빌드해서 확인해야 해서 어쩔 수 없이 속도가 많이 느리다.

반응형
Comments