[Android] AndroidJUnit4

 ๐Ÿ’ก view ํ…Œ์ŠคํŠธ(Recycler, Detail…)๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ AndroidJUnit4๋ฅผ ํ•™์Šตํ•˜์˜€์Šต๋‹ˆ๋‹ค!

AndroidJUnit4

  • Android ๊ฐœ๋ฐœ ์ค‘ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ JUnit4 ํ…Œ์ŠคํŠธ ๋Ÿฌ๋„ˆ์ž…๋‹ˆ๋‹ค.
  • AndroidJUnit4๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋ ˆ์ž„์›Œํฌ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์„ฑ ์š”์†Œ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • Activity, Service, ContentProvider..
  • ํ…Œ์ŠคํŠธ ์ค‘์— Android API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์— @RunWith(AndroidJUnit4::class) ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ AndroidJUnit4 ํ…Œ์ŠคํŠธ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.myapp", appContext.packageName)
    }
}

ํŠน์ง•

  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ง€์›
    • Android ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์—ฌ๋Ÿฌ ๊ตฌ์„ฑ ์š”์†Œ ๊ฐ„์˜ ์ƒํ˜ธ ์ž‘์šฉ์„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Instrumentation ํ…Œ์ŠคํŠธ
    • ์‹ค์ œ ๊ธฐ๊ธฐ ๋˜๋Š” ์• ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์‹คํ–‰๋˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Android API ์‚ฌ์šฉ
    • ํ…Œ์ŠคํŠธ ์ค‘์— Android API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ActivityScenarioRule

  • AndroidX Test ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ผ๋ถ€๋กœ, Activity ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์‰ฝ๊ฒŒ ์ œ์–ดํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ๊ฐ€ ์‹œ์ž‘๋  ๋•Œ ์ง€์ •๋œ Activity๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์‹œ์ž‘ํ•˜๋ฉฐ, ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด Activity๋ฅผ ์ž๋™์œผ๋กœ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๋ช…์‹œ์ ์œผ๋กœ Activity๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ •๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค.
  • ๋ชจ๋“  AndroidX Activity ๋ฐ Fragment ํ™œ๋™๊ณผ ํ˜ธํ™˜๋˜๋ฉฐ, ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ ์šฉํ•˜๊ธฐ

  • ActivityScenarioRule์„ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
dependencies {
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test:rules:1.4.0'
    androidTestImplementation 'androidx.test:core:1.4.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
  • ์ดํ›„์— ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์—์„œ ActivityScenarioRule๋ฅผ ์ •์˜ํ•˜๊ณ  ์‚ฌ์šฉํ•  Activity๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
 @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)
  • ์ด๋•Œ @get:Rule ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ผญ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์–ด๋…ธํ…Œ์ด์…˜์€ activityRule์„ ํ…Œ์ŠคํŠธ ์‹œ์ž‘ ์ „์— ์‹คํ–‰ํ•˜๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

์ฃผ์š” ๋ฉ”์„œ๋“œ

  • launch() : ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  • moveToState() : ์•กํ‹ฐ๋น„ํ‹ฐ์˜ ๋ผ์ด๋ธŒ์‚ฌ์ดํด ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
  • recreate() : ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์žฌ์ƒ์„ฑ ํ•ฉ๋‹ˆ๋‹ค.
  • close() : ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.

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

  • ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ Android ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด์„œ View(Fragment)๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์ƒ์„ฑ

@RunWith(AndroidJUnit4::class)
class ProductDetailFragmentTest 
  • ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์–ด๋…ธํ…Œ์ด์…˜์„ ์ ์šฉํ•ด์„œ AndroidJUnit4 ํ…Œ์ŠคํŠธ์ž„์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.

TestFixture & ActivityScenarioRule

  • ์•กํ‹ฐ๋น„ํ‹ฐ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๋”ฐ๋ฅด๋Š” ํ”„๋ž˜๊ทธ๋จผํŠธ๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ActivityScenarioRule์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ๋ฐ˜๋ณต ์ž‘์—…(TestFixture)๋ฅผ ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค.
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Before
    fun setUp() {
        val productDetailFragment =
            ProductDetailFragment().apply {
                arguments = ProductDetailFragment.createBundle(3L)
            }
        activityRule.scenario.onActivity { activity ->
            activity.supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, productDetailFragment)
                .commitNow()
        }
    }
  • ํ•ด๋‹น ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ ProductDetailFragment๋กœ ์ด๋™ํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

    @Test
    fun `์„ ํƒํ•œ_์ƒํ’ˆ์˜_์ด๋ฆ„์ด_๋ณด์—ฌ์ ธ์•ผ_ํ•œ๋‹ค`() {
        onView(withId(R.id.tv_detail_product_name)).check(matches(isDisplayed()))
    }

    @Test
    fun `์„ ํƒํ•œ_์ƒํ’ˆ์˜_๊ฐ€๊ฒฉ์ด_๋ณด์—ฌ์ ธ์•ผ_ํ•œ๋‹ค`() {
        onView(withId(R.id.tv_detail_product_price)).check(matches(withText("0์›")))
    }

    @Test
    fun `์ˆ˜๋Ÿ‰์„_์ฆ๊ฐ€ํ•˜๋ฉด_์„ ํƒํ•œ_์ˆ˜๋Ÿ‰์˜_๊ฐœ์ˆ˜๊ฐ€_๋ณด์—ฌ์ ธ์•ผ_ํ•œ๋‹ค`() {
        onView(withId(R.id.btn_add_count)).perform(click())

        onView(withId(R.id.tv_count)).check(matches(withText("1")))
    }
  • ํ”„๋ž˜๊ทธ๋จผํŠธ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๋ทฐ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๊ทธ๋ ค์ง€๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ check() ๋ฉ”์„œ๋“œ์™€ ํด๋ฆญ ์‹œ ๋™์ž‘์„ ํ™•์ธํ•˜๋Š” perform() ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ 

https://developer.android.com/reference/androidx/test/runner/AndroidJUnit4

https://developer.android.com/training/testing/local-tests?hl=ko