๐ก ์ฐ์ํํ ํฌ์ฝ์ค ๊ณผ์ ์ ์งํํ๋ฉด์ ๋ฐฐ์ ๋ 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
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