본문 바로가기

Android

Android Test : Ui Test - Espresso

728x90

효율적인 안드로이드 개발을 위해선 테스트에 대한 것을 빼놓을 수 없다. 안드로이드에서 UI Test 방법으로 Espresso, UI Automator 등이 있다.

 

UI 테스트를 위해선 어떤 프레임워크를 사용할 지 정해야 한다.

Espresso를 선택하였으니, build.gradle 파일에 해당 라이브러리가 추가되어 있는지 확인하고 없다면 추가한 후 테스트를 시작하면 된다.

 

defaultConfig {
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 }

dependencies {
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.test:runner:1.3.0'
}

이렇게하면 앱에서 Android Instrumentation Runner가 설정됩니다.

AndroidJUnitRunner 는 계측 실행기입니다. 이것은 본질적으로 전체 테스트 제품군을 실행하는 진입 점입니다. 테스트 환경, 테스트 APK를 제어하고 테스트 패키지에 정의 된 모든 테스트를 시작합니다.

프로젝트를 생성한 초기에 Unit, UI 테스트 디렉토리 내에 ExampleUnitTest, ExampleInstrumentedTest 파일이 있는 것을 확인할 수 있다.

UI 테스트를 위해서 새로운 테스트 Class를 생성하고 @RunWith (AndroidJUnit4 :: class)를 Annotation을 Class 위에 추가하면 기본적인 준비는 끝난다.

module-name/app/src/androidTest/java/.../

Espresso

에스프레소는 구글에서 제작하고 제공하는 안드로이드 UI를 자동으로 테스트하는 프레임워크

Espresso Annotation

@RunWith : 이 어노테이션으로 설정된 Class 러너를 결정하고, 초기화 한 다음 해당 Class에서 테스트를 실행하는데 사용된다. Android의 경우 AndroidJUnitRunner는 AndroidJUnit4 Class Runner가 구성 매개 변수를 전달할 수 있도록 설정되어 있는 명시적으로 확인한다.

@Before : @Test를 시작하기 전 사전에 진행해야할 정의에 해당된다. @Test가 시작되기 전 항상 호출되게 된다.

@After : 모든 테스트가 종료되면 호출된다. 메모리에서 resource를 release 할 수 있다.

@Test : @Before가 완료된 후 실제 테스트를 수행하도록 한다.

@Rule : 해당 Test class에서 사용하게 될 AcitivtyTestRule과 ServiceTestRule에 대하여 정의한다.

@BeforeClass, @AfterClass : 테스트 전, 후로 테스트 클래스에서 딱 한번씩만 수행된다.

@RequiresDevice : 에뮬레이터에선 테스트가 불가능하고, 오직 기기만 사용하도록 한다.

@SdkSupress : minSdkVersion을 지정할 수 있다.

@SmallTest, @MediumTest, @LargeTest : 테스트 성격을 구분하여 테스트 할 수 있다.

View Matchers

withId(), withText() 등 메서드가 존재한다.

onView(Matcher<View> viewMatcher) 메서드를 사용해 View 계층 구조에서 특정 뷰를 찾을 수 있다. 인수를 Matcher로 사용하지만 모든 UI 요소를 찾을 수 있도록 속성이 포함되어 있다.

withId()​ 메서드를 사용하여 id 값에 맞는 View를 찾아 사용할 수 있다.

View Actions

click(), typeText() 등 메서드가 존재한다.

View를 찾은 후 ViewAcitions를 사용하여 View에서 작업을 수행할 수 있다.

View Assertions

matches() 등 메서드가 존재한다.

View에서 작업을 수행한 후 View가 원하는대로 동작하는지 확인할 수 있다.

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="Your Activity">

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/text_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:text="hello world"
        app:layout_constraintBottom_toTopOf="@+id/btn_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit_text" />

    <Button
        android:id="@+id/btn_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text_view" />


</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
    lateinit var editText : EditText
    lateinit var textView : TextView
    lateinit var button : Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        editText = findViewById<EditText>(R.id.edit_text)
        textView = findViewById<TextView>(R.id.text_view)
        button = findViewById<Button>(R.id.btn_view)

        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(p0: Editable?) {
            }
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            }
            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                textView.text = p0
            }
        })

        button.setOnClickListener {
            Toast.makeText(applicationContext, textView.text, Toast.LENGTH_SHORT).show()
        }
    }
}
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class EspressoTest {
    @Rule @JvmField var activityTestRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun myTest(){
        // edit_text라는 id를 갖고있는 View를 찾아 hello를 입력하고, keyboard를 닫는다.
      onView(withId(R.id.edit_text)).perform(typeText("hello"), closeSoftKeyboard())

        // text_view라는 id를 갖고있는 View를 찾아 현재 담긴 값이 hello인지 확인한다.
        onView(withId(R.id.text_view)).check(matches(withText("hello")))

        // btn_view라는 id를 갖고있는 Button을 클릭 이벤트를 발생시킨다.
        onView(withId(R.id.btn_view))
            .perform(click())
    }

}