[Android] Espresso๋ฅผ ํ™œ์šฉํ•œ UI Test

๐Ÿ’ก ์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ฐฐ์› ๋˜ UI ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•˜์—ฌ ๋‹ค์‹œ ํ•œ๋ฒˆ ๋Œ์•„๋ณด๊ณ  ํ•™์Šตํ•˜๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์กŒ์Šต๋‹ˆ๋‹ค !

 

UI ํ…Œ์ŠคํŠธ

  • ์‚ฌ์šฉ์ž์™€ ์•ฑ ๊ฐ„์— ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด UI ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • UI ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ๋ณต์žกํ•œ UI ๋กœ์ง์ด๋‚˜ ๊ธฐ์กด์— ๊ฐœ๋ฐœ๋˜์–ด ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ํ•„์š”์„ฑ

  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ์žฅ์• ์— ๊ด€ํ•œ ์‹ ์†ํ•œ ํ”ผ๋“œ๋ฐฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐœ๋ฐœ ์ฃผ๊ธฐ์—์„œ ์ดˆ๊ธฐ ์žฅ์• ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ , ๋” ์•ˆ์ „ํ•œ ์ฝ”๋“œ ๋ฆฌํŒฉํ„ฐ๋ง์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ๋˜ํ•œ ๊ธฐ์ˆ ์  ๋ฌธ์ œ๋ฅผ ์ตœ์†Œํ™”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์ •์ ์ธ ๊ฐœ๋ฐœ ์†๋„๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณ„์ธก ํ…Œ์ŠคํŠธ(Instrumentation Test)

  • ์•ฑ์˜ ์‹ค์ œ ๋””๋ฐ”์ด์Šค ๋˜๋Š” ์• ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ๋™์ž‘ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • ์• ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ์ผœ๊ณ  ์•ฑ์„ ๋นŒ๋“œํ•˜์—ฌ ์‹คํ–‰ํ•˜๊ณ  ์กฐ์ž‘ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ UI ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๊ณผ์ •์€ ํ™•์ธํ•ด์•ผ ํ•  ์‚ฌํ•ญ์ด ๋งŽ๊ฑฐ๋‚˜, ๋ฒˆ๊ฑฐ๋กœ์šธ ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ์— ์†Œ๋ชจ๋˜๋Š” ์‹œ๊ฐ„์ด ํฌ๊ฒŒ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด UI ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Espresso

  • Espresso๋Š” ๊ฐ„๊ฒฐํ•˜๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” Android Ui Test๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ ์ž…๋‹ˆ๋‹ค.
  • UI ์ƒํ˜ธ์ž‘์šฉ์„ ์‹คํ–‰ํ•  ๋•Œ ์ž‘์—…์„ ๋™๊ธฐํ™”๋œ ์ƒํƒœ๋กœ ์œ ์ง€ํ•ด ์ฃผ๋ฉฐ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ ๊ทธ ์ž‘์—…์ด ์™„๋ฃŒ ๋œ ๋’ค์— ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • ์•ˆ์ •์ ์ธ ํ™˜๊ฒฝ์—์„œ UI Test๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  • ๊ฐ„๊ฒฐํ•˜๊ณ  ์ง๊ด€์ ์ธ API๋ฅผ ์ œ๊ณต
    • UI ์š”์†Œ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ฑฐ๋‚˜ ์š”์†Œ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋™๊ธฐํ™” ์ง€์›
    • Espresso๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฉ”์ธ ์“ฐ๋ ˆ๋“œ์™€ ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”๋˜์–ด, UI ํ…Œ์ŠคํŠธ ์ค‘ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ƒํƒœ๊ฐ€ ์•ˆ์ •์ ์ผ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋ฅผ ํ†ตํ•ด ์‹ ๋ขฐ์„ฑ ์žˆ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ํ’๋ถ€ํ•œ Matcher, ViewAction ์ œ๊ณต
    • ๋‹ค์–‘ํ•œ Matcher์™€ View Actions์„ ์ œ๊ณตํ•˜์—ฌ ํŠน์ • ๋ทฐ๋ฅผ ์ฐพ๊ฑฐ๋‚˜ ๋ทฐ์—์„œ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • onView(withId(R.id.button))์™€ ๊ฐ™์ด ๋ทฐ๋ฅผ ์ฐพ๊ณ , perform(click())์œผ๋กœ ํด๋ฆญ ์•ก์…˜์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ ์ฆ๊ฐ€
    • ์ž์—ฐ์–ด์— ๊ฐ€๊นŒ์šด ๊ตฌ๋ฌธ์œผ๋กœ ํ…Œ์ŠคํŠธ์˜ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ™•์žฅ์„ฑ
    • ์ปค์Šคํ…€ Matcher, ViewAction, IdlingResource ๋“ฑ์„ ์ž‘์„ฑํ•˜์—ฌ Espresso์˜ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Espresso ์ค€๋น„ํ•˜๊ธฐ

  • Espresso๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Gradle์— ์„ ์–ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
  • multiModule ๊ตฌ์กฐ๋ผ๋ฉด UI Test๋ฅผ ์ง„ํ–‰ ํ•  ๋ชจ๋“ˆ์— ํ•ด๋‹น ์ข…์† ํ•ญ๋ชฉ์„ ์„ ์–ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ๋•Œ ์ฃผ์˜ํ•  ์ ์€ androidx๋กœ๋งŒ ์„ ์–ธํ•ด์•ผ ํ•˜๋ฉฐ, ๊ทธ๋ ‡์ง€ ์•Š์„ ๊ฒฝ์šฐ build ๊ณผ์ •์—์„œ ์ œ๋Œ€๋กœ ๋œ context๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•˜๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

onView

  • onView๋Š” ํ…Œ์ŠคํŠธ ๋Œ€์ƒ UI ์š”์†Œ๋ฅผ ์ฐพ๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • onView(withId(R.id.text_view))์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • [withId] : UI ์š”์†Œ๋ฅผ ์ฐพ๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋งค์นญ ์กฐ๊ฑด์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

check

  • check๋Š” ํ…Œ์ŠคํŠธ ๋Œ€์ƒ UI ์š”์†Œ์— ๋Œ€ํ•œ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • check(matches(withText(”Hello World!”)))์™€ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • [matches] : ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฐ’๊ณผ ์‹ค์ œ ๊ฐ’์„ ๋น„๊ตํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋งค์นญ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.
  • [withText] : UI ์š”์†Œ์˜ ํ…์ŠคํŠธ๋ฅผ ๋น„๊ตํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋งค์นญ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.

๋‹ค์–‘ํ•œ View Test

EditText ์ž…๋ ฅํ•˜๊ธฐ

  • EditText View์— ์ž…๋ ฅ๊ฐ’์„ ๋„ฃ์–ด์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
onView(withId(R.id.edit_text))
    .check(matches(isDisplayed()))
    .perform(typeText("์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค"))

allOf ํ™œ์šฉ

  • allOf๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ณด๋‹ค ์ •ํ™•ํ•˜๊ฒŒ View๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
onView(allOf(withId(R.id.text_view), withText("์ฐพ๋Š” ํ…์ŠคํŠธ")))

View ์Šคํฌ๋กค ์ ์šฉ

  • ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉ์ž๊ฐ€ ๋ทฐ๋ฅผ ์Šคํฌ๋กค ํ–ˆ์„ ๋•Œ ๋™์ž‘์„ ์ œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// when: ์‚ฌ์šฉ์ž์˜ ์Šคํฌ๋กค ๋™์ž‘ ์ˆ˜ํ–‰
onView(withId(R.id.scroll_view))
    .perform(swipeUp())

 // then: ํ™”๋ฉด์— ๋ฒ„ํŠผ ํ‘œ์‹œ
onView(withId(R.id.button))
    .check(matches(isDisplayed()))

Assertion

  • ์กฐ๊ฑด์— ๋งž๋Š” View๋ฅผ ์ฐพ์•˜๋‹ค๋ฉด, matches๋ฅผ ํ†ตํ•ด์„œ ๊ฒ€์ฆ๊ณผ ํ™•์ธ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
onView(withId(R.id.text_view))
    .check(matches(withText("์ฐพ๋Š” ํ…์ŠคํŠธ")))

์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ์›ํ• ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ

  • View์— ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋‹จ๋… ํ…Œ์ŠคํŠธ ์‹œ Pass ๋˜์ง€๋งŒ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด Fail์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธฐ๋Š”๋ฐ, ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์œผ๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
android {
    testOptions {
        animationsDisabled = true
    }
}

ํ…Œ์ŠคํŠธ ์ง„ํ–‰

  • ์ด์ „์— ๋ฐฐ์šด Junit4 ํ•™์Šต์„ ์ฐธ๊ณ ํ•˜์—ฌ ์‹ค์Šต์„ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • https://jinudmjournal.tistory.com/189
 

[Android] AndroidJUnit4

๐Ÿ’ก view ํ…Œ์ŠคํŠธ(Recycler, Detail…)๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ AndroidJUnit4๋ฅผ ํ•™์Šตํ•˜์˜€์Šต๋‹ˆ๋‹ค!AndroidJUnit4Android ๊ฐœ๋ฐœ ์ค‘ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ JUnit4 ํ…Œ์ŠคํŠธ ๋Ÿฌ๋„ˆ์ž…๋‹ˆ๋‹ค.AndroidJUnit4๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์•ˆ๋“œ๋กœ์ด๋“œ

jinudmjournal.tistory.com

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.*
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 LoginActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun testLoginSuccess() {
        // ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ
        onView(withId(R.id.username)).perform(typeText("admin"), closeSoftKeyboard())
        onView(withId(R.id.password)).perform(typeText("1234"), closeSoftKeyboard())

        // ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ
        onView(withId(R.id.login_button)).perform(click())

        // ๊ฒฐ๊ณผ ํ…์ŠคํŠธ ํ™•์ธ
        onView(withId(R.id.result_text)).check(matches(withText("Login Success")))
    }

    @Test
    fun testLoginFailed() {
        // ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ
        onView(withId(R.id.username)).perform(typeText("user"), closeSoftKeyboard())
        onView(withId(R.id.password)).perform(typeText("wrongpassword"), closeSoftKeyboard())

        // ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ
        onView(withId(R.id.login_button)).perform(click())

        // ๊ฒฐ๊ณผ ํ…์ŠคํŠธ ํ™•์ธ
        onView(withId(R.id.result_text)).check(matches(withText("Login Failed")))
    }
}

์ •๋ฆฌ

  • Espresso๋Š” ๊ฐ„๋‹จํ•˜๋ฉด์„œ ๊ฐ•๋ ฅํ•œ UI ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ๋‹ค์–‘ํ•œ UI ์ƒํ˜ธ์ž‘์šฉ๊ณผ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฐ„๋‹จํ•œ Test๋ฅผ ๊ฒ€์ฆํ•˜์˜€๋Š”๋ฐ, ๋” ๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค(๋ฆฌ์ŠคํŠธ, ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ๋“ฑ)์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ๊ฐ€๋Šฅํ•˜๋ฉฐ ๋‹ค์Œ ํ•™์Šต์—์„œ ์ง„ํ–‰ํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค !
  • Espresso ์š”์•ฝ๋ณธ์œผ๋กœ ํ•œ๋ˆˆ์— Espresso ๊ด€๋ จ ๋กœ์ง์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • https://developer.android.com/training/testing/espresso/cheat-sheet?hl=ko

์ฐธ๊ณ 

https://arc.net/l/quote/gvricusx

https://dev.to/wise4rmgod/unit-testing-using-robolectric-in-android-studio-2ibl

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