엄코딩의 개발 일지

UI 테스트 코드를 작성하게된 이유

 

유닛테스트와 마찬가지로 수동으로 테스트하게 되면, 인력 소모와 테스트의 효율성, 안정성을 보장하기 어렵습니다.

 

그리고 최근에는 모바일앱이 점점 거대해지고, 복잡해지면서 각각의 기능을 분리하여 개발하게 되고,

 

모듈화된 feature들을 merge하는 경우가 많습니다. 

 

이에 효율적인 테스트 코드 작성이 가능해진다면 복잡한 앱이라도 더 쉬운 테스트, 효율적인 테스트가 가능할 것입니다.

 

이 포스팅의 목적

 

이번 포스팅은 이전 포스팅인 유닛테스트 ( https://eso0609.tistory.com/77 ) 와 다르게 UI 테스트를 목적으로 작성했습니다.

 

구글링을 통해서 학습하고, 정리한 내용은 Kotlin으로 작성해 보았습니다.

 

예제 코드는 간단하게 이름, 아이디, 비밀번호를 입력하고, 하단 textView에 입력된 정보가 맞게 들어갔는지 확인합니다.

 

마지막으로 버튼을 클릭하여 입력된 정보를 Toast 메세지로 뿌려줍니다.

 

프로젝트 구조

UI 테스트를 위해 src/androidTest에 프로젝트를 생성해줍니다.

 

build.gradle

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.espressotest"
        minSdkVersion 24
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    androidTestImplementation 'com.android.support.test:rules:1.0.2'
}

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        android:orientation="vertical"
        android:padding="16dp"
        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=".MainActivity">

    <LinearLayout
            android:weightSum="4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/name_tv"
                android:layout_weight="1"
                android:textSize="16sp"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="NAME"/>
        <EditText
                android:id="@+id/name_et"
                android:layout_weight="3"
                android:layout_marginLeft="8dp"
                android:textSize="16sp"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:hint="Input name .."/>

    </LinearLayout>

    <LinearLayout
            android:layout_marginTop="16dp"
            android:weightSum="4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/id_tv"
                android:layout_weight="1"
                android:textSize="16sp"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="ID"/>
        <EditText
                android:id="@+id/id_et"
                android:layout_weight="3"
                android:layout_marginLeft="8dp"
                android:textSize="16sp"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:hint="Input id .."/>

    </LinearLayout>

    <LinearLayout
            android:layout_marginTop="16dp"
            android:weightSum="4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/pwd_tv"
                android:layout_weight="1"
                android:textSize="16sp"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:text="PWD"/>
        <EditText
                android:id="@+id/pwd_et"
                android:layout_weight="3"
                android:layout_marginLeft="8dp"
                android:textSize="16sp"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:hint="Input password .."/>

    </LinearLayout>

    <Button
            android:id="@+id/done_btn"
            android:text="DONE"
            android:layout_marginTop="16dp"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    <LinearLayout
            android:weightSum="3"
            android:layout_marginTop="16dp"
            android:id="@+id/board_linear"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <TextView
                android:id="@+id/name_board_tv"
                android:textSize="16sp"
                android:textStyle="bold"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"/>
        <TextView
                android:layout_marginTop="4dp"
                android:id="@+id/id_board_tv"
                android:textSize="16sp"
                android:textStyle="bold"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"/>
        <TextView
                android:layout_marginTop="4dp"
                android:id="@+id/pwd_board_tv"
                android:textSize="16sp"
                android:textStyle="bold"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"/>


    </LinearLayout>

</LinearLayout>

 

MainActivity.class

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

        name_et.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) {
                name_board_tv.text = "NAME : $p0"
            }
        })

        id_et.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) {
                id_board_tv.text = "ID : $p0"
            }
        })

        pwd_et.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) {
                pwd_board_tv.text = "PWD : $p0"
            }
        })

        done_btn.setOnClickListener {
            Toast.makeText(
                this,
                "이름 : ${name_board_tv.text}\nID : ${id_board_tv.text}\nPWD : ${pwd_board_tv.text}",
                Toast.LENGTH_LONG
            ).show()
        }
    }
}

 

EspressoTest.class

import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions.*
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.support.test.filters.LargeTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

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

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


        @Test
        fun startTest(){

                //editText에 값 입력후 키보드 닫음.
                onView(withId(R.id.name_et)).perform(typeText("qwertyui"))
                onView(withId(R.id.id_et)).perform(typeText("ace69"))
                onView(withId(R.id.pwd_et)).perform(typeText("123456"), closeSoftKeyboard())


                //textView 값이 Hello Wordl! 인지 확인
                onView(withId(R.id.name_board_tv)).check(matches(withText("NAME : qwertyui")))
                onView(withId(R.id.id_board_tv)).check(matches(withText("ID : ace69")))
                onView(withId(R.id.pwd_board_tv)).check(matches(withText("PWD : 123456")))

                //Button 클릭
                onView(withId(R.id.done_btn)).perform(click())
        }
}

 

실행

이번 Espresso UI테스트의 실행은 안드로이드 단말기 또는 에뮬레이터로 실행하게됩니다.

 

EspressoTest 우클릭 Run ~ 하시면됩니다.

 

 

실행결과

 

 

처음에 샤오미 단말기로 테스트해보았는데, 계속해서 timeout에러가 났습니다.

 

이 부분에 대해서는 더 학습해보고 포스팅 해볼 예정입니다.

 

( 이번 테스트 기기는 갤럭시 s10+였습니다. )

 

 

참고 자료

 

https://developer.android.com/training/testing/espresso