Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AdventureResultViewModel 테스트 추가 #338

Merged
merged 8 commits into from
Sep 20, 2023
4 changes: 4 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dependencies {
// coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

// splash
implementation "androidx.core:core-splashscreen:1.0.0"
Expand All @@ -105,4 +106,7 @@ dependencies {

// lottie
implementation "com.airbnb.android:lottie:6.1.0"

// ViewModel Test
testImplementation("androidx.arch.core:core-testing:2.2.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.now.naaga

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.now.domain.model.AdventureResult
import com.now.domain.model.AdventureResultType
import com.now.domain.model.Coordinate
import com.now.domain.model.Place
import com.now.domain.model.Player
import com.now.domain.model.Rank
import com.now.domain.repository.AdventureRepository
import com.now.domain.repository.RankRepository
import com.now.naaga.presentation.adventureresult.AdventureResultViewModel
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime

class AdventureResultViewModelTest {
private lateinit var vm: AdventureResultViewModel
private lateinit var adventureRepository: AdventureRepository
private lateinit var rankRepository: RankRepository

@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()

private val fakeAdventureResultOnSuccess = AdventureResult(
id = 1,
gameId = 1,
destination = Place(
id = 1,
name = "파이브 가이즈",
coordinate = Coordinate(37.1234, 125.1234),
image = "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn" +
".net%2Fdn%2FcPs9Im%2Fbtrb2k1feQq%2FBW34tbqjtHscUYkgCPyjcK%2Fimg.jpg,",
description = "룰루랄라",
),
resultType = AdventureResultType.SUCCESS,
score = 1000,
playTime = 30,
distance = 200,
hintUses = 3,
tryCount = 2,
beginTime = LocalDateTime.of(
LocalDate.of(2023, 9, 16),
LocalTime.of(13, 30),
),
endTime = LocalDateTime.of(
LocalDate.of(2023, 9, 16),
LocalTime.of(14, 0),
),
)

private val fakeAdventureResultOnFailure = AdventureResult(
id = 2,
gameId = 1,
destination = Place(
id = 1,
name = "파이브 가이즈",
coordinate = Coordinate(37.1234, 125.1234),
image = "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net" +
"%2Fdn%2FcPs9Im%2Fbtrb2k1feQq%2FBW34tbqjtHscUYkgCPyjcK%2Fimg.jpg,",
description = "룰루랄라",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

신나시나봐요 저한테 할 말 없으신가요? ^^

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋ빅스는 개인 면담 잡으세요^^

),
resultType = AdventureResultType.FAIL,
score = 1000,
playTime = 30,
distance = 200,
hintUses = 3,
tryCount = 2,
beginTime = LocalDateTime.of(
LocalDate.of(2023, 9, 16),
LocalTime.of(13, 30),
),
endTime = LocalDateTime.of(
LocalDate.of(2023, 9, 16),
LocalTime.of(14, 0),
),
)

private val fakeMyRank = Rank(
player = Player(
id = 1,
nickname = "뽀또",
score = 1000,
),
rank = 1,
percent = 1,
)

@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setup() {
Dispatchers.setMain(UnconfinedTestDispatcher())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P4] 이 부분과 tearDown()에서 resetMain() 해주는 이유가 무엇인지 설명해주실 수 있으신가요?
저도 뷰모델 테스트에 대해 알아볼 때 코루틴 때문에 쓰는 것으로 봤지만 자세히 학습하지 않아서 여쭤봅니다.

Copy link
Collaborator Author

@hyunji1203 hyunji1203 Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단위 테스트에서는 Dispatcher.Main을 사용할 수 없다고 합니다!
공식 문서의 말로는 Android 기기가 아닌 로컬 JVM에서 실행되기 때문이라고 하네요👀
그래서 별개의 디스패쳐를 넣어준 후 해당 디스패쳐를 사용해 테스트를 돌리기 위해 setMain과 resetMain을 해주는 것 이라고 이해했습니다~

해당 부분을 설명해주는 공식 문서 링크도 첨부하겠습니다!

adventureRepository = mockk()
rankRepository = mockk()
vm = AdventureResultViewModel(adventureRepository, rankRepository)
}

@OptIn(ExperimentalCoroutinesApi::class)
@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun `성공한 게임 결과 불러오기`() {
// given
coEvery {
adventureRepository.fetchAdventureResult(1)
} coAnswers {
fakeAdventureResultOnSuccess
}
Comment on lines +119 to +123
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P5] runTest{} 를 사용할 수도 있다는 점 알려드립니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오~ 꿀팁 감사합니다! 다음 뷰모델 테스트에선 runTest 사용해보겠습니다!


// when
vm.fetchGameResult(1)

// then
assertEquals(vm.adventureResult.getOrAwaitValue(), fakeAdventureResultOnSuccess)
assertEquals(vm.adventureResult.getOrAwaitValue().resultType, AdventureResultType.SUCCESS)
}

@Test
fun `실패한 게임 결과 불러오기`() {
// given
coEvery {
adventureRepository.fetchAdventureResult(2)
} coAnswers {
fakeAdventureResultOnFailure
}

// when
vm.fetchGameResult(2)

// then
assertEquals(vm.adventureResult.getOrAwaitValue(), fakeAdventureResultOnFailure)
assertEquals(vm.adventureResult.getOrAwaitValue().resultType, AdventureResultType.FAIL)
}

@Test
fun `내 랭킹 결과 불러오기`() {
// given
coEvery {
rankRepository.getMyRank()
} coAnswers {
fakeMyRank
}

// when
vm.fetchMyRank()

// then
assertEquals(vm.myRank.getOrAwaitValue(), fakeMyRank.rank)
}
}
32 changes: 32 additions & 0 deletions android/app/src/test/java/com/now/naaga/getOrAwaitValue.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.now.naaga

import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T) {
data = o
latch.countDown()
[email protected](this)
}
}

this.observeForever(observer)

// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}

@Suppress("UNCHECKED_CAST")
return data as T
}
Loading