Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 2주차 미션

## 🎲 1단계
### GithubProfileRepository
+ 로컬에 유저 정보가 있음을 확인할 수 있다
+ 로컬에 유저 정보가 없음을 확인할 수 있다
+ 로컬에 유저 정보가 이미 존재할 때 유저 정보를 반환할 수 있다
+ 유저 정보가 없을 때 서버로부터 유저 정보를 받아오는 것을 확인할 수 있다
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.malibin.study.github.presentation.sample

import androidx.lifecycle.*
import kotlinx.coroutines.launch

interface CountMemory {
fun save(history: String)
fun getHistories(): List<String>
}

class TestViewModel(
private val countMemory: CountMemory,
) : ViewModel() {

private val _count = MutableLiveData<Int>(0)
val count: LiveData<Int> = _count

private val _histories = MutableLiveData<List<String>>()
val histories: LiveData<List<String>> = _histories

fun increase() {
_count.value = _count.value?.plus(1)
countMemory.save("increase")
}

fun decrease() {
_count.value = _count.value?.minus(1)
countMemory.save("decrease")
}

fun loadHistories() {
_histories.value = countMemory.getHistories()
}
}


//=======================================================================================


interface CountMemoryWithCoroutine {
suspend fun save(history: String)
suspend fun getHistories(): List<String>
}

class TestViewModelWithCoroutine(
private val countMemory: CountMemoryWithCoroutine,
) : ViewModel() {

private val _count = MutableLiveData<Int>(0)
val count: LiveData<Int> = _count

private val _histories = MutableLiveData<List<String>>()
val histories: LiveData<List<String>> = _histories

fun increase() {
viewModelScope.launch {
_count.value = _count.value?.plus(1)
countMemory.save("increase")
}
}

fun decrease() {
viewModelScope.launch {
_count.value = _count.value?.minus(1)
countMemory.save("decrease")
}
}

fun loadHistories() {
viewModelScope.launch {
_histories.value = countMemory.getHistories()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ class TodoManager(
) {
private val currentTodos = mutableListOf<String>()

fun getTodos(): List<String> {
suspend fun getTodos(): List<String> {
return currentTodos.toList()
}

fun getTodoHistories(): List<String> {
suspend fun getTodoHistories(): List<String> {
return todoMemory.getHistory()
}

fun createTodo(todo: String) {
suspend fun createTodo(todo: String) {
currentTodos.add(todo)
todoMemory.create(todo)
}

fun finishTodo(todo: String) {
suspend fun finishTodo(todo: String) {
currentTodos.remove(todo)
todoMemory.finish(todo)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.malibin.study.github.data.repository

import com.google.common.truth.Truth.assertThat
import com.malibin.study.github.data.source.GithubProfileSource
import com.malibin.study.github.domain.profile.GithubProfile
import com.malibin.study.github.domain.repository.GithubProfileRepository
import com.malibin.study.github.utils.InstantTaskExecutorExtension
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

internal class DefaultGithubProfileRepositoryTest {

private lateinit var fakeLocalSource: GithubProfileSource
private lateinit var fakeRemoteSource: GithubProfileSource

companion object {
@JvmStatic
@RegisterExtension
val instantTaskExecutorExtension = InstantTaskExecutorExtension()
}
Comment on lines +21 to +25
Copy link
Member

Choose a reason for hiding this comment

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

이 extension은 안드로이드 요소 (LiveData) 등이 끼어있는 유닛테스트를 할 때 추가해주시면 돼요!


@BeforeEach
fun setUp() {
fakeLocalSource = mockk<GithubProfileSource>(relaxed = true)
fakeRemoteSource = mockk<GithubProfileSource>(relaxed = true)
}

@Test
fun `로컬에 유저 정보가 있음을 확인할 수 있다`() = runBlocking {
// given
val expectedName = "stopkite"

coEvery { fakeLocalSource.getGithubProfile("stopkite").getOrThrow().userName } returns "stopkite"

Comment on lines +35 to +39
Copy link
Member

Choose a reason for hiding this comment

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

.getGithubProfile()이 반환하는 것이 GithubProfile 객체이기에, 이 객체를 모두 하드코딩으로 직접 만들어주는 것이 바람직해요!

모킹은 작성해주신 것 처럼 체이닝을 길게 써도 가능은 하지만, 원하는 객체가 기대하는 값을 반환하게 적는 게 가장 좋아요 :)

// when
val actualSearch = fakeLocalSource.getGithubProfile("stopkite").getOrThrow().userName

// then
assertThat(actualSearch).isEqualTo(expectedName)
}

@Test
fun `로컬에 유저 정보가 없음을 확인할 수 있다`() = runBlocking {
Comment on lines +47 to +48
Copy link
Member

Choose a reason for hiding this comment

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

Repository에서는 로컬에 유저정보가 있는 지 없는 지에 대한 정보를 외부에 알리지 않고 있어요.
내부적으로만 활용하고 있기 때문에 이 Repository에 대한 테스트라고 보기엔 어려워요.

getGithubProfile을 할 때 repository에서 무슨 일이 벌어지고 있는 지 테스트해보는 것은 어떨까요?

// given
val expectedName = "stopkite"

coEvery { fakeLocalSource.getGithubProfile("stopkite").getOrThrow().userName } returns ""

// when
val actualSearch = fakeLocalSource.getGithubProfile("stopkite").getOrThrow().userName

// then
assertThat(actualSearch).isNotEqualTo(expectedName)
}

@Test
fun `로컬에 유저 정보가 이미 존재할 때 유저 정보를 반환할 수 있다`() = runBlocking {
// given
val githubProfile = GithubProfile(
0,
"stopkite",
"https://avatars.githubusercontent.com/u/62979643?v=4",
"Ji-Yeon",
"1",
10,
100
)

coEvery { fakeLocalSource.getGithubProfile("stopkite") } returns runCatching { githubProfile }
Comment on lines +61 to +74
Copy link
Member

Choose a reason for hiding this comment

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

remote source의 메서드는 정말로 호출하지 않았는 지 테스트해보는 것도 좋겠네요 :)


// when
val actualLocalProfile = fakeLocalSource.getGithubProfile("stopkite").getOrThrow()
Comment on lines +76 to +77
Copy link
Member

Choose a reason for hiding this comment

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

getOrThrow를 하면 정말로 throw가 발생할 수도 있기 때문에, (test fail)
getOrNull을 활용해보는 것은 어떨까요?
assertThat()에 들어가는 타입은 null이어도 괜찮아요 :)


// then
assertThat(actualLocalProfile).isEqualTo(githubProfile)
}

@Test
fun `유저 정보가 없을 때 서버로부터 유저 정보를 받아오는 것을 확인할 수 있다`() = runBlocking {
// given
Comment on lines +83 to +85
Copy link
Member

Choose a reason for hiding this comment

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

getGithubProfile을 할 때 유저 정보가 없음을 Repository는 무엇을 통해 알았는지 한 번 생각해보면 좋겠어요 :)

val expectedGithubProfile = GithubProfile(
0,
"stopkite",
"https://avatars.githubusercontent.com/u/62979643?v=4",
"Ji-Yeon",
"1",
10,
100
)

coEvery {
fakeRemoteSource.getGithubProfile("stopkite")
} returns runCatching { expectedGithubProfile }

// when
val actualSavedLocalProfile = fakeRemoteSource.getGithubProfile("stopkite").getOrThrow()

// then
assertThat(actualSavedLocalProfile).isEqualTo(expectedGithubProfile)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.malibin.study.github.presentation.sample

import com.google.common.truth.Truth.assertThat
import com.malibin.study.github.utils.InstantTaskExecutorExtension
import com.malibin.study.github.utils.MainCoroutineExtension
import com.malibin.study.github.utils.getOrAwaitValue
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.extension.RegisterExtension

internal class TestViewModelTest {

companion object {
@JvmStatic
@RegisterExtension
val instantTaskExecutorExtension = InstantTaskExecutorExtension()
}

private lateinit var countMemory: CountMemory
private lateinit var testViewModel: TestViewModel

@BeforeEach
fun setUp() {
countMemory = mockk<CountMemory>(relaxed = true)
testViewModel = TestViewModel(countMemory)
}

@Test
fun test1() {
// when
testViewModel.increase()

// then
assertAll(
{ assertThat(testViewModel.count.value).isEqualTo(1) },
{ verify(exactly = 1) { countMemory.save("increase") } } // increase 가 한 번 호출되었다
)

}


}

@OptIn(ExperimentalCoroutinesApi::class)
internal class TestViewModelWithCoroutineTest {

companion object {
@JvmStatic
@RegisterExtension
val instantTaskExecutorExtension = InstantTaskExecutorExtension()

@JvmStatic
@RegisterExtension
val mce = MainCoroutineExtension()
}

private lateinit var countMemory: CountMemoryWithCoroutine
private lateinit var testViewModel: TestViewModelWithCoroutine

@BeforeEach
fun setUp() {
countMemory = mockk<CountMemoryWithCoroutine>(relaxed = true)
testViewModel = TestViewModelWithCoroutine(countMemory)
}

@Test
fun test1() = runBlocking {
// when
testViewModel.increase()

// then
assertAll(
{ assertThat(testViewModel.count.getOrAwaitValue()).isEqualTo(1) },
{ coVerify(exactly = 1) { countMemory.save("increase") } } // increase 가 한 번 호출되었다
)

}


}
Loading