๐ก ์ฐ์ํํ ํฌ์ฝ์ค ๊ณผ์ ์์ ์งํํ 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())
}
}
์ฐธ๊ณ
https://medium.com/@igorwojda/kotlin-introduction-to-testing-android-presenters-e038f1064e04