MockK์„ ํ™œ์šฉํ•œ Presenter ํ…Œ์ŠคํŠธ

๐Ÿ’ก ์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ๊ณผ์ •์—์„œ ์ง„ํ–‰ํ•œ mocking์„ ํ™œ์šฉํ•œ Presenter ํ…Œ์ŠคํŠธ๋ฅผ ๊ธฐ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

๊ฐœ์š”

  • MVC to MVP ๊ณผ์ •์„ ํ†ตํ•ด์„œ MVP ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๊ตฌํ˜„ ํ›„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ„ฐ๋ง ํ•˜๋Š” ๊ณผ์ •์—์„œ Presenter ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•˜์—ฌ ํ•™์Šตํ•˜์˜€๊ณ , ๊ทธ ๊ณผ์ •์„ ๊ธฐ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค !
  • ์ด์ „ ๊ธฐ์ˆ˜์˜ ์ˆ˜๋‹ฌ๋‹˜์˜ ์˜์ƒ์„ ๋งŽ์ด ์ฐธ๊ณ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

[10๋ถ„ ํ…Œ์ฝ”ํ†ก] ์ˆ˜๋‹ฌ์˜ mock์„ ์‚ฌ์šฉํ•ด android presenter testํ•˜๊ธฐ

 

Mock

  • Mock์€ Test Double์˜ ์ข…๋ฅ˜ ์ค‘ ํ•˜๋‚˜๋กœ ํ˜ธ์ถœ์— ๋Œ€ํ•œ ๊ธฐ๋Œ€๋ฅผ ๋ช…์„ธํ•˜๊ณ  ๋‚ด์šฉ์— ๋”ฐ๋ผ ๋™์ž‘ํ•˜๋„๋ก ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋œ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.
  • ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ๋Š” Mockito ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•ด Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ƒ์—์„œ ์–ด๋–ค ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ• ์ง€ ๊ฒฐ์ •์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

Test Double

  • ํ…Œ์ŠคํŠธ ๋”๋ธ”์ด๋ž€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ ์ด๋ฅผ ๋Œ€์‹ ํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๊ฐ์ฒด๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค.
  • ํŠน์ • ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›๋Š” ์ด์œ ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ค์šด ๊ฒฝ์šฐ, ํ…Œ์ŠคํŠธํ•˜๋ ค๋Š” ๊ฐ์ฒด์™€ ์—ฐ๊ด€๋œ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ ์ด๋ฅผ ๋Œ€์‹ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค.

Kotlin ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ์‹œ mock ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด Java์—์„œ๋Š” Mockito๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Kotlin์—์„œ๋Š” Mockito์™€ ์œ ์‚ฌํ•œ MockK ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์กด์žฌํ•˜๋ฉฐ, Java์˜ Mockito ์‚ฌ์šฉ๋ฒ•๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

์˜์กด์„ฑ ์ถ”๊ฐ€

  • MockK๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  dependency ์ถ”๊ฐ€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
testImplementation "io.mockk:mockk:$mockk_version"
androidTestImplementation "io.mockk:mockk-android:$mockk_version"

MockK ๋ฉ”์„œ๋“œ

every()

  • every() ํ•จ์ˆ˜๋Š” mock ๊ฐ์ฒด๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ์ง€ ์ •์˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • returns ํ•จ์ˆ˜๋กœ ํŠน์ • ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜์‹œ์ผœ์ค„ ์ˆ˜ ์žˆ๊ณ , throws๋กœ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// mockObject.someMethod()๊ฐ€ ํ˜ธ์ถœ ์‹œ "Mocked Value"๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •
every { mockObject.someMethod() } returns "Mocked Value"

verify()

  • mock ๊ฐ์ฒด์˜ ํŠน์ • ๋ฉ”์„œ๋“œ๊ฐ€ ์ง€์ •๋œ ํšŸ์ˆ˜๋งŒํผ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
// mockObject.someMethod()๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
verify { mockObject.someMethod() }

relaxed()

  • relaxed = true๋กœ ์„ค์ •ํ•˜๋ฉด ์ •์˜๋˜์ง€ ์•Š์€ ๋ชจ๋“  ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.
// mockObject์˜ ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋Š” ํ˜ธ์ถœ์ด ์ž๋™์œผ๋กœ ๊ธฐ๋ณธ ๊ฐ’์„ ๋ฐ˜ํ™˜
val mockObject = mockk<MyClass>(relaxed = true)

slot()

  • ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์ค‘ ์ธ์ž๋ฅผ ์บก์ฒ˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
val slot = slot<String>()
every { mockObject.someMethod(capture(slot)) } returns
  • someMethod์— ์ „๋‹ฌ ๋œ ์ธ์ž๊ฐ€ slot์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

coEvery(), coVerify()

  • ์ฝ”ํˆฌ๋ฆฐ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์„œ๋“œ๋‚˜ ํ•จ์ˆ˜์˜ ๋™์ž‘์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
// suspendMethod์— ๋Œ€ํ•ด mock ๋™์ž‘์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ ๊ฒ€์ฆํ•œ๋‹ค.
coEvery { mockObject.suspendMethod() } returns "Mocked Value"
coVerify { mockObject.suspendMethod() }

clearMocks()

  • ํ˜ธ์ถœ ๊ธฐ๋ก์ด๋‚˜ ์„ค์ •๋œ ๊ธฐ๋ก์„ ํฌํ•จํ•œ mock ๊ฐ์ฒด์˜ ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
clearMocks(mockObject)

spyk()

  • ์‹ค์ œ ๊ฐ์ฒด๋ฅผ spy ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • spy ๊ฐ์ฒด๋Š” ์‹ค์ œ ๊ฐ์ฒด์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด์„œ๋„, ํŠน์ • ๋ฉ”์„œ๋“œ๋Š” mock ๋™์ž‘์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// someMethod ํ˜ธ์ถœ ๋Œ€์‹  Mocked value ๋ฐ˜ํ™˜
val spiedObject = spyk<MyClass>()
every { spiedObject.someMethod() } returns "Mocked Value"

justRun()

  • ํŠน์ • ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ ์•„๋ฌด ๋™์ž‘๋„ ํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
// ์ผ๋ฐ˜์ ์œผ๋กœ Unit์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
justRun { mockObject.someMethod() }

mockkStatic(), mockkObject()

  • ์ •์  ๋ฉ”์„œ๋“œ๋‚˜ ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด๋ฅผ mock์œผ๋กœ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
mockkStatic("com.example.MyClassKt")
every { someStaticFunction() } returns "Mocked Value"

Test ๋ฒ”์œ„ ์ง€์ •

  • ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์— ๋”ฐ๋ผ ์›ํ•˜๋Š” ํ–‰๋™์„ ํ•˜๊ณ  ๋ทฐ๊ฐ€ ๋…ธ์ถœ๋˜๋Š”์ง€๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • HTTP API ํ†ต์‹  ์‘๋‹ต ๊ฐ’๊ณผ ์„ฑ๊ณต ๋ฐ ์‹คํŒจ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๊ฐ’์„ ์ž˜ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฒˆ ์ฃผ์ œ์—์„œ๋Š” View๋‚˜ ํ†ต์‹ ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์ด ์•„๋‹Œ Presenter์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Presenter ํ…Œ์ŠคํŠธ

  • ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ MVP ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์šฐ์„  ์•„๋ž˜์™€ ๊ฐ™์€ MVP ํŒจํ„ด ๊ตฌ์กฐ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  • Presenter์˜ ์ค‘์š” ๋กœ์ง์ธ loadData()๋ฅผ ์ฃผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.
interface MainView { // ๋ทฐ ์ธํ„ฐํŽ˜์ด์Šค
    fun showData(data: String)
    fun showError(message: String)
}

 class GetMainUseCase { // ์œ ์ฆˆ์ผ€์ด์Šค
    fun load() : String = "Sample Data"
}

class MainPresenter( // Presenter
    private val view: MainView,
    private val getMainUseCase: GetMainUseCase,
) {
    fun loadData() {
        val data = getMainUseCase.load()
        if (data.isNotEmpty()) {
            view.showData(data)
        } else {
            view.showError("No data available")
        }
    }
}

์ดˆ๊ธฐํ™” ์ž‘์—…

  • View ์ธํ„ฐํŽ˜์ด์Šค์™€ useCase๋ฅผ @Mock์œผ๋กœ mocking์„ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด์„œ Mock ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•˜๋ฉฐ, ๋‘ ๊ฐ์ฒด๋Š” ์‹ค์ œ ์ธ์Šคํ„ด์Šค๊ฐ€ ์•„๋‹Œ Mockito๊ฐ€ ์ƒ์„ฑํ•œ ๊ฐ€์งœ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.
import org.junit.Before
import org.junit.Test
import org.mockito.Mock

class MainPresenterTest {
    @Mock
    private lateinit var getMainUseCase: GetMainUseCase
    @Mock
    private val mainView: MainView

    private lateinit var presenter: MainPresenter

    @Before
    fun setup() {
        // MockitoAnnotations.openMocks(this)
        mainView = mockk()
        getMainUseCase = mockk()
        presenter = MainPresenter(mockView,getMainUseCase)
    }
}
  • @Before ๋ฉ”์„œ๋“œ์—์„œ Mockito๋ฅผ ์ดˆ๊ธฐํ•˜์—ฌ ๋ชจ๋“  ๊ฐ์ฒด๋“ค์„ ์ดˆ๊ธฐํ™”ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • Presenter์— ์ดˆ๊ธฐํ™”๋œ Mock ๊ฐ์ฒด๋“ค์„ ์ฃผ์ž…ํ•˜์—ฌ MainPresenter๋ฅผ ์ƒ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ

  • “should show data when data is available”
    • GetMainUseCase๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ MainView์˜ showData()๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , showError()๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • “should show error when data is not available”
    • GetMainUserCase๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ showError()๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ , showData()๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

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

import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations

class MainPresenterTest {

    @Test
    fun `should show data when data is available`() {
        // Given
        val sampleData = "Sample Data"
        `when`(getMainUseCase.load()).thenReturn(sampleData)

        // When
        presenter.loadData()

        // Then
        verify(mainView).showData(sampleData)
        verify(mainView, never()).showError(anyString())
    }

    @Test
    fun `should show error when data is not available`() {
        // Given
        `when`(getMainUseCase.load()).thenReturn("")

        // When
        presenter.loadData()

        // Then
        verify(mainView).showError("No data available")
        verify(mainView, never()).showData(anyString())
    }
}

 

์ฐธ๊ณ 

Test Double์„ ์•Œ์•„๋ณด์ž

https://medium.com/@igorwojda/kotlin-introduction-to-testing-android-presenters-e038f1064e04

https://blog.banksalad.com/tech/test-in-banksalad-android/

https://site.mockito.org/

https://leveloper.tistory.com/199