From c34fc193995b71e97e122f7db3b362e438257b49 Mon Sep 17 00:00:00 2001 From: Wooyeol Lee Date: Thu, 30 Nov 2023 00:08:42 +0900 Subject: [PATCH 1/4] test: apply testing on myCreatedEmojiList and mySavedEmojiList with fake EmojiList --- .../java/com/goliath/emojihub/models/Emoji.kt | 30 +++++++ .../emojihub/usecases/EmojiUseCaseImplTest.kt | 85 +++++++++++++++++-- .../test/java/com/goliath/emojihub/utils.kt | 33 +++++++ 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/goliath/emojihub/models/Emoji.kt b/android/app/src/main/java/com/goliath/emojihub/models/Emoji.kt index 154cd14c..0951581f 100644 --- a/android/app/src/main/java/com/goliath/emojihub/models/Emoji.kt +++ b/android/app/src/main/java/com/goliath/emojihub/models/Emoji.kt @@ -14,6 +14,36 @@ class Emoji( val unicode: String = dto.unicode val label: String = dto.label val id: String = dto.id + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Emoji + if (createdBy != other.createdBy) return false + if (isSaved != other.isSaved) return false + if (createdAt != other.createdAt) return false + if (savedCount != other.savedCount) return false + if (videoLink != other.videoLink) return false + if (thumbnailLink != other.thumbnailLink) return false + if (unicode != other.unicode) return false + if (label != other.label) return false + if (id != other.id) return false + + return true + } + override fun hashCode(): Int { + var result = createdBy.hashCode() + result = 31 * result + isSaved.hashCode() + result = 31 * result + createdAt.hashCode() + result = 31 * result + savedCount + result = 31 * result + videoLink.hashCode() + result = 31 * result + thumbnailLink.hashCode() + result = 31 * result + unicode.hashCode() + result = 31 * result + label.hashCode() + result = 31 * result + id.hashCode() + return result + } } data class EmojiDto( diff --git a/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt index 4e00fa19..6f79a115 100644 --- a/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt @@ -3,11 +3,11 @@ package com.goliath.emojihub.usecases import androidx.paging.PagingData import androidx.paging.map import androidx.paging.testing.asSnapshot +import com.goliath.emojihub.createDeterministicDummyEmojiDtoList import com.goliath.emojihub.data_sources.ApiErrorController import com.goliath.emojihub.mockLogClass import com.goliath.emojihub.models.CreatedEmoji import com.goliath.emojihub.models.Emoji -import com.goliath.emojihub.models.EmojiDto import com.goliath.emojihub.models.UploadEmojiDto import com.goliath.emojihub.repositories.local.X3dRepository import com.goliath.emojihub.repositories.remote.EmojiRepository @@ -17,7 +17,6 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import org.junit.Assert.* @@ -52,10 +51,30 @@ class EmojiUseCaseImplTest { assertEquals(samplePagingEmojiData, emojiUseCaseImpl.emojiList.value) } + @Test + fun updateMyCreatedEmojiList_withSamplePagingEmojiData_updatesMyCreatedEmojiListStateFlow() { + // given + val samplePagingEmojiData = mockk>() + // when + runBlocking { emojiUseCaseImpl.updateMyCreatedEmojiList(samplePagingEmojiData) } + // then + assertEquals(samplePagingEmojiData, emojiUseCaseImpl.myCreatedEmojiList.value) + } + + @Test + fun updateMySavedEmojiList_withSamplePagingEmojiData_updatesMySavedEmojiListStateFlow() { + // given + val samplePagingEmojiData = mockk>() + // when + runBlocking { emojiUseCaseImpl.updateMySavedEmojiList(samplePagingEmojiData) } + // then + assertEquals(samplePagingEmojiData, emojiUseCaseImpl.mySavedEmojiList.value) + } + @Test fun fetchEmojiList_returnsFlowOfEmojiPagingData() { // given - val sampleEmojiPagingDataFlow = spyk>>() + val sampleEmojiPagingDataFlow = createDeterministicDummyEmojiDtoList(5) val sampleAnswer = sampleEmojiPagingDataFlow.map { it.map { dto -> Emoji(dto) } } coEvery { emojiRepository.fetchEmojiList() @@ -65,10 +84,62 @@ class EmojiUseCaseImplTest { // then coVerify(exactly = 1) { emojiRepository.fetchEmojiList() } runBlocking { - assertEquals( - sampleAnswer.asSnapshot(), - fetchedEmojiPagingDataFlow.asSnapshot() - ) + val sampleAnswerAsSnapshot = sampleAnswer.asSnapshot() + val fetchedEmojiPagingDataFlowAsSnapshot = fetchedEmojiPagingDataFlow.asSnapshot() + for (i in sampleAnswerAsSnapshot.indices) { + assertEquals( + sampleAnswerAsSnapshot[i], + fetchedEmojiPagingDataFlowAsSnapshot[i] + ) + } + } + } + + @Test + fun fetchMyCreatedEmojiList_returnsFlowOfEmojiPagingData() { + // given + val sampleEmojiPagingDataFlow = createDeterministicDummyEmojiDtoList(5) + val sampleAnswer = sampleEmojiPagingDataFlow.map { it.map { dto -> Emoji(dto) } } + coEvery { + emojiRepository.fetchMyCreatedEmojiList() + } returns sampleEmojiPagingDataFlow + // when + val fetchedEmojiPagingDataFlow = runBlocking { emojiUseCaseImpl.fetchMyCreatedEmojiList() } + // then + coVerify(exactly = 1) { emojiRepository.fetchMyCreatedEmojiList() } + runBlocking { + val sampleAnswerAsSnapshot = sampleAnswer.asSnapshot() + val fetchedEmojiPagingDataFlowAsSnapshot = fetchedEmojiPagingDataFlow.asSnapshot() + for (i in sampleAnswerAsSnapshot.indices) { + assertEquals( + sampleAnswerAsSnapshot[i], + fetchedEmojiPagingDataFlowAsSnapshot[i] + ) + } + } + } + + @Test + fun fetchMySavedEmojiList_returnsFlowOfEmojiPagingData() { + // given + val sampleEmojiPagingDataFlow = createDeterministicDummyEmojiDtoList(5) + val sampleAnswer = sampleEmojiPagingDataFlow.map { it.map { dto -> Emoji(dto) } } + coEvery { + emojiRepository.fetchMySavedEmojiList() + } returns sampleEmojiPagingDataFlow + // when + val fetchedEmojiPagingDataFlow = runBlocking { emojiUseCaseImpl.fetchMySavedEmojiList() } + // then + coVerify(exactly = 1) { emojiRepository.fetchMySavedEmojiList() } + runBlocking { + val sampleAnswerAsSnapshot = sampleAnswer.asSnapshot() + val fetchedEmojiPagingDataFlowAsSnapshot = fetchedEmojiPagingDataFlow.asSnapshot() + for (i in sampleAnswerAsSnapshot.indices) { + assertEquals( + sampleAnswerAsSnapshot[i], + fetchedEmojiPagingDataFlowAsSnapshot[i] + ) + } } } diff --git a/android/app/src/test/java/com/goliath/emojihub/utils.kt b/android/app/src/test/java/com/goliath/emojihub/utils.kt index 16b263da..fc2a2622 100644 --- a/android/app/src/test/java/com/goliath/emojihub/utils.kt +++ b/android/app/src/test/java/com/goliath/emojihub/utils.kt @@ -1,8 +1,15 @@ package com.goliath.emojihub import android.util.Log +import androidx.paging.PagingData +import androidx.paging.map +import com.goliath.emojihub.models.Emoji +import com.goliath.emojihub.models.EmojiDto import io.mockk.every import io.mockk.mockkStatic +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map fun mockLogClass() { mockkStatic(Log::class) @@ -10,4 +17,30 @@ fun mockLogClass() { every { Log.d(any(), any()) } returns 0 every { Log.i(any(), any()) } returns 0 every { Log.e(any(), any()) } returns 0 +} + +val dummyUsernames = listOf("channn", "doggydog", "meow_0w0", "mpunchmm", "kick_back") +val dummyUnicodes = listOf("U+1F44D", "U+1F600", "U+1F970", "U+1F60E", "U+1F621", "U+1F63A", "U+1F496", "U+1F415") +const val dummyMaxSavedCounts = 2000 +fun createDeterministicDummyEmojiDtoList(listSize : Int): Flow> { + val dummyEmojiList = mutableListOf() + for (i in 0 until listSize) { + dummyEmojiList.add( + EmojiDto( + createdBy = dummyUsernames[i % dummyUsernames.size], + createdAt = "2023.09.16", + savedCount = dummyMaxSavedCounts % (i + 1), + videoLink = "", + thumbnailLink = "", + unicode = dummyUnicodes[i % dummyUnicodes.size], + id = "1234", + label = "sample" + ) + ) + } + return flowOf(PagingData.from(dummyEmojiList)) +} + +fun createDeterministicDummyEmojiList(listSize: Int): Flow> { + return createDeterministicDummyEmojiDtoList(listSize).map { it.map { dto -> Emoji(dto) } } } \ No newline at end of file From 61fbb09e3b96989cddb3afb3404a0b0f5cb098ee Mon Sep 17 00:00:00 2001 From: Wooyeol Lee Date: Thu, 30 Nov 2023 00:09:23 +0900 Subject: [PATCH 2/4] refactor: remove redundant stubbing on spy object --- .../repositories/remote/EmojiRepositoryImplTest.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt index a2d7de37..7cf5958d 100644 --- a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt @@ -84,9 +84,6 @@ class EmojiRepositoryImplTest { every { emojiRepositoryImpl.createVideoThumbnail(any(), any()) } returns File("sampleThumbnailFile") - coEvery { - emojiRepositoryImpl.uploadEmoji(any(), any()) - } answers { callOriginal() } // when val isUploaded = runBlocking { @@ -111,9 +108,6 @@ class EmojiRepositoryImplTest { every { emojiRepositoryImpl.createVideoThumbnail(any(), any()) } returns File("sampleThumbnailFile") - coEvery { - emojiRepositoryImpl.uploadEmoji(any(), any()) - } answers { callOriginal() } // when val isUploaded = runBlocking { @@ -138,9 +132,6 @@ class EmojiRepositoryImplTest { every { emojiRepositoryImpl.createVideoThumbnail(any(), any()) } returns File("sampleThumbnailFile") - coEvery { - emojiRepositoryImpl.uploadEmoji(any(), any()) - } answers { callOriginal() } // when val isUploaded = runBlocking { @@ -165,9 +156,6 @@ class EmojiRepositoryImplTest { every { emojiRepositoryImpl.createVideoThumbnail(any(), any()) } returns File("sampleThumbnailFile") - coEvery { - emojiRepositoryImpl.uploadEmoji(any(), any()) - } answers { callOriginal() } // when val isUploaded = runBlocking { From cb26a9abeda7d41cb3ae654aee2d2189f77e6836 Mon Sep 17 00:00:00 2001 From: Wooyeol Lee Date: Thu, 30 Nov 2023 00:12:50 +0900 Subject: [PATCH 3/4] test: apply testing on myCreatedEmojiList and mySavedEmojiList with fake Api Response with fake EmojiList body --- .../remote/EmojiRepositoryImplTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt index 7cf5958d..10ce54a6 100644 --- a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt @@ -65,6 +65,48 @@ class EmojiRepositoryImplTest { } } + @Test + fun fetchMyCreatedEmojiList_returnsFlowOfPagingDataOfEmojiDto() { + // given + val numSampleEmojis = 10 + val sampleEmojiDtoList = List(numSampleEmojis) { sampleEmojiDto } + val expectedFetchedEmojiDtoList = List(numSampleEmojis*2) { sampleEmojiDto } + // *2 because of .asSnapshot() load one more time + coEvery { + emojiApi.fetchMyCreatedEmojiList(any(), any(), any()) + } returns Response.success(sampleEmojiDtoList) + // when + val fetchedEmojiPagingDataFlow = runBlocking { emojiRepositoryImpl.fetchMyCreatedEmojiList() } + val fetchedEmojiDtoList = runBlocking { fetchedEmojiPagingDataFlow.asSnapshot() } + // then + coVerify(exactly = 2) { emojiApi.fetchMyCreatedEmojiList(any(), any(), any()) } + runBlocking { + assertEquals(expectedFetchedEmojiDtoList.size, fetchedEmojiDtoList.size) + assertEquals(expectedFetchedEmojiDtoList, fetchedEmojiDtoList) + } + } + + @Test + fun fetchMySavedEmojiList_returnsFlowOfPagingDataOfEmojiDto() { + // given + val numSampleEmojis = 10 + val sampleEmojiDtoList = List(numSampleEmojis) { sampleEmojiDto } + val expectedFetchedEmojiDtoList = List(numSampleEmojis*2) { sampleEmojiDto } + // *2 because of .asSnapshot() load one more time + coEvery { + emojiApi.fetchMySavedEmojiList(any(), any(), any()) + } returns Response.success(sampleEmojiDtoList) + // when + val fetchedEmojiPagingDataFlow = runBlocking { emojiRepositoryImpl.fetchMySavedEmojiList() } + val fetchedEmojiDtoList = runBlocking { fetchedEmojiPagingDataFlow.asSnapshot() } + // then + coVerify(exactly = 2) { emojiApi.fetchMySavedEmojiList(any(), any(), any()) } + runBlocking { + assertEquals(expectedFetchedEmojiDtoList.size, fetchedEmojiDtoList.size) + assertEquals(expectedFetchedEmojiDtoList, fetchedEmojiDtoList) + } + } + // @Test fun getEmojiWithId() { TODO("Not yet implemented") From ba9261cd32c729d0df23af49baa7d951d9a85e6d Mon Sep 17 00:00:00 2001 From: Wooyeol Lee Date: Thu, 30 Nov 2023 00:15:10 +0900 Subject: [PATCH 4/4] test: apply testing on EmojiViewModel --- .../emojihub/viewmodels/EmojiViewModel.kt | 9 +- .../emojihub/viewmodels/EmojiViewModelTest.kt | 189 ++++++++++++++++++ 2 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 android/app/src/test/java/com/goliath/emojihub/viewmodels/EmojiViewModelTest.kt diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt index 0319ae9a..41c07e19 100644 --- a/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt @@ -22,15 +22,16 @@ import javax.inject.Inject class EmojiViewModel @Inject constructor( private val emojiUseCase: EmojiUseCase ): ViewModel() { - var videoUri: Uri = Uri.EMPTY + lateinit var videoUri: Uri var currentEmoji: Emoji? = null var isBottomSheetShown by mutableStateOf(false) val emojiList = emojiUseCase.emojiList val myCreatedEmojiList = emojiUseCase.myCreatedEmojiList val mySavedEmojiList = emojiUseCase.mySavedEmojiList - - private val _topK = 3 + companion object { + private const val _topK = 3 + } fun fetchEmojiList() { viewModelScope.launch { @@ -79,6 +80,6 @@ class EmojiViewModel @Inject constructor( } suspend fun unSaveEmoji(id: String) { - emojiUseCase.saveEmoji(id) + emojiUseCase.unSaveEmoji(id) } } \ No newline at end of file diff --git a/android/app/src/test/java/com/goliath/emojihub/viewmodels/EmojiViewModelTest.kt b/android/app/src/test/java/com/goliath/emojihub/viewmodels/EmojiViewModelTest.kt new file mode 100644 index 00000000..550bc488 --- /dev/null +++ b/android/app/src/test/java/com/goliath/emojihub/viewmodels/EmojiViewModelTest.kt @@ -0,0 +1,189 @@ +package com.goliath.emojihub.viewmodels + +import android.net.Uri +import com.goliath.emojihub.createDeterministicDummyEmojiList +import com.goliath.emojihub.mockLogClass +import com.goliath.emojihub.models.CreatedEmoji +import com.goliath.emojihub.usecases.EmojiUseCase +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.* +import org.junit.Before + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.io.File + +@RunWith(JUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class EmojiViewModelTest { + private val emojiUseCase = spyk() + private val emojiViewModel = EmojiViewModel(emojiUseCase) + + private val testDispatcher = StandardTestDispatcher() + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + mockLogClass() + } + @After + fun cleanUp() { + Dispatchers.resetMain() + } + + @Test + fun fetchEmojiList_success_updateEmojiList() = runTest { + // given + val sampleFetchedEmojiList = createDeterministicDummyEmojiList(10) + coEvery { + emojiUseCase.fetchEmojiList() + } returns sampleFetchedEmojiList + // when + emojiViewModel.fetchEmojiList() + advanceUntilIdle() + // then + coVerify(exactly = 1) { emojiUseCase.fetchEmojiList() } + coVerify(exactly = 1) { emojiUseCase.updateEmojiList(any()) } + } + + @Test + fun fetchMyCreatedEmojiList_success_updateMyCreatedEmojiList() = runTest { + // given + val sampleFetchedMyCreatedEmojiList = createDeterministicDummyEmojiList(10) + coEvery { + emojiUseCase.fetchMyCreatedEmojiList() + } returns sampleFetchedMyCreatedEmojiList + // when + emojiViewModel.fetchMyCreatedEmojiList() + advanceUntilIdle() + // then + coVerify(exactly = 1) { emojiUseCase.fetchMyCreatedEmojiList() } + coVerify(exactly = 1) { emojiUseCase.updateMyCreatedEmojiList(any()) } + } + + @Test + fun fetchMySavedEmojiList_success_updateMySavedEmojiList() = runTest { + // given + val sampleFetchedMySavedEmojiList = createDeterministicDummyEmojiList(10) + coEvery { + emojiUseCase.fetchMySavedEmojiList() + } returns sampleFetchedMySavedEmojiList + // when + emojiViewModel.fetchMySavedEmojiList() + advanceUntilIdle() + // then + coVerify(exactly = 1) { emojiUseCase.fetchMySavedEmojiList() } + coVerify(exactly = 1) { emojiUseCase.updateMySavedEmojiList(any()) } + } + + @Test + fun createEmoji_success_returnsListOfTopKCreatedEmoji() { + // given + val videoUri = spyk() + val sampleEmojiList = listOf( + CreatedEmoji("id1", "unicode1"), + CreatedEmoji("id2", "unicode2"), + CreatedEmoji("id3", "unicode3") + ) + coEvery { + emojiUseCase.createEmoji(any(), any()) + } returns sampleEmojiList + // when + val createdEmojiList: List = runBlocking { + emojiViewModel.createEmoji(videoUri) + } + // then + coVerify { emojiUseCase.createEmoji(videoUri, any()) } + assertEquals(sampleEmojiList, createdEmojiList) + } + + @Test + fun createEmoji_failure_returnsEmptyList() { + // given + val videoUri = spyk() + coEvery { + emojiUseCase.createEmoji(any(), any()) + } returns emptyList() + // when + val createdEmojiList: List = runBlocking { + emojiViewModel.createEmoji(videoUri) + } + // then + coVerify { emojiUseCase.createEmoji(videoUri, any()) } + assertEquals(emptyList(), createdEmojiList) + } + + @Test + fun uploadEmoji_success_returnsTrue() { + // given + val emojiUnicode = "unicode" + val emojiLabel = "label" + val videoFile = mockk() + coEvery { + emojiUseCase.uploadEmoji(any(), any(), any()) + } returns true + // when + val isUploaded = runBlocking { + emojiViewModel.uploadEmoji(emojiUnicode, emojiLabel, videoFile) + } + // then + coVerify { emojiUseCase.uploadEmoji(emojiUnicode, emojiLabel, videoFile) } + assertTrue(isUploaded) + } + + @Test + fun uploadEmoji_failure_returnsFalse() { + // given + val emojiUnicode = "unicode" + val emojiLabel = "label" + val videoFile = mockk() + coEvery { + emojiUseCase.uploadEmoji(any(), any(), any()) + } returns false + // when + val isUploaded = runBlocking { + emojiViewModel.uploadEmoji(emojiUnicode, emojiLabel, videoFile) + } + // then + coVerify { emojiUseCase.uploadEmoji(emojiUnicode, emojiLabel, videoFile) } + assertFalse(isUploaded) + } + + @Test + fun saveEmoji_success_returnsUnit() { + // given + val id = "sampleId" + coEvery { + emojiUseCase.saveEmoji(any()) + } returns true + // when + runBlocking { emojiViewModel.saveEmoji(id) } + // then + coVerify { emojiUseCase.saveEmoji(id) } + } + + @Test + fun unSaveEmoji_success_returnsUnit() { + // given + val id = "sampleId" + coEvery { + emojiUseCase.unSaveEmoji(any()) + } returns true + // when + runBlocking { emojiViewModel.unSaveEmoji(id) } + // then + coVerify { emojiUseCase.unSaveEmoji(id) } + } +} \ No newline at end of file