From 74cfb869d684cecb3067bf55521abe7d99da8ede Mon Sep 17 00:00:00 2001 From: Michael Lien Date: Mon, 1 Feb 2021 19:12:15 +0800 Subject: [PATCH] Remove unused method and refactor Board Api tests --- app/build.gradle | 14 ++ .../tw/y_studio/ptt/di/DataSourceModules.kt | 2 +- .../remote/board/IPopularRemoteDataSource.kt | 6 - .../board/PopularRemoteDataSourceImpl.kt | 15 +- .../ptt/ui/hot_board/HotBoardsViewModel.kt | 1 - .../java/tw/y_studio/ptt/MainCoroutineRule.kt | 29 ++++ .../java/tw/y_studio/ptt/TestJsonFileUtils.kt | 27 ++++ .../board/PopularRemoteDataSourceTest.kt | 53 +++---- .../resources/api/board/popular_boards.json | 23 +++ versions.gradle | 135 +++++++++--------- 10 files changed, 192 insertions(+), 113 deletions(-) create mode 100644 app/src/sharedTest/java/tw/y_studio/ptt/MainCoroutineRule.kt create mode 100644 app/src/sharedTest/java/tw/y_studio/ptt/TestJsonFileUtils.kt create mode 100644 app/src/test/resources/api/board/popular_boards.json diff --git a/app/build.gradle b/app/build.gradle index 1828dd5..8c22724 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,6 +44,16 @@ android { buildFeatures { viewBinding true } + + sourceSets { + String sharedTestDir = 'src/sharedTest/java' + test { + java.srcDir sharedTestDir + } + androidTest { + java.srcDir sharedTestDir + } + } } def domain() { @@ -129,11 +139,15 @@ dependencies { testImplementation deps.google.truth testImplementation deps.junit testImplementation deps.mockK.core + testImplementation deps.androidX.arch + testImplementation deps.kotlin.coroutines.test androidTestImplementation deps.androidX.test.ext.junit androidTestImplementation deps.androidX.test.ext.espresso androidTestImplementation deps.google.truth androidTestImplementation deps.mockK.android + androidTestImplementation deps.androidX.arch + androidTestImplementation deps.kotlin.coroutines.test debugImplementation deps.square.leakcanary diff --git a/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt b/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt index 52eeb25..312af1d 100644 --- a/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt +++ b/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt @@ -9,7 +9,7 @@ import tw.y_studio.ptt.source.remote.search.ISearchBoardRemoteDataSource import tw.y_studio.ptt.source.remote.search.SearchBoardRemoteDataSourceImpl val dataSourceModules = module { - factory { PopularRemoteDataSourceImpl(get(), get()) } + factory { PopularRemoteDataSourceImpl(get()) } factory { SearchBoardRemoteDataSourceImpl(get()) } factory { PostRemoteDataSourceImpl(get()) } } diff --git a/app/src/main/java/tw/y_studio/ptt/source/remote/board/IPopularRemoteDataSource.kt b/app/src/main/java/tw/y_studio/ptt/source/remote/board/IPopularRemoteDataSource.kt index 6be382a..241585f 100644 --- a/app/src/main/java/tw/y_studio/ptt/source/remote/board/IPopularRemoteDataSource.kt +++ b/app/src/main/java/tw/y_studio/ptt/source/remote/board/IPopularRemoteDataSource.kt @@ -1,13 +1,7 @@ package tw.y_studio.ptt.source.remote.board import tw.y_studio.ptt.api.model.hot_board.HotBoard -import tw.y_studio.ptt.api.model.hot_board.HotBoardTemp interface IPopularRemoteDataSource { - - fun getPopularBoardData(page: Int, count: Int): MutableList - suspend fun getPopularBoards(): HotBoard - - fun disposeAll() } diff --git a/app/src/main/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceImpl.kt b/app/src/main/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceImpl.kt index f34ef53..5835e0a 100644 --- a/app/src/main/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceImpl.kt +++ b/app/src/main/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceImpl.kt @@ -1,25 +1,12 @@ package tw.y_studio.ptt.source.remote.board -import tw.y_studio.ptt.api.PopularBoardListAPI import tw.y_studio.ptt.api.board.BoardApiService import tw.y_studio.ptt.api.model.hot_board.HotBoard -import tw.y_studio.ptt.api.model.hot_board.HotBoardTemp class PopularRemoteDataSourceImpl( - private val boardApiService: BoardApiService, - private val popularBoardListAPI: PopularBoardListAPI + private val boardApiService: BoardApiService ) : IPopularRemoteDataSource { - - @Throws(Exception::class) - override fun getPopularBoardData(page: Int, count: Int): MutableList { - return popularBoardListAPI.refresh(page, count) - } - override suspend fun getPopularBoards(): HotBoard { return boardApiService.getPopularBoard() } - - override fun disposeAll() { - popularBoardListAPI.close() - } } diff --git a/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt b/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt index ea0dde3..8893939 100644 --- a/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt +++ b/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt @@ -46,6 +46,5 @@ class HotBoardsViewModel( override fun onCleared() { super.onCleared() - popularRemoteDataSource.disposeAll() } } diff --git a/app/src/sharedTest/java/tw/y_studio/ptt/MainCoroutineRule.kt b/app/src/sharedTest/java/tw/y_studio/ptt/MainCoroutineRule.kt new file mode 100644 index 0000000..83cd118 --- /dev/null +++ b/app/src/sharedTest/java/tw/y_studio/ptt/MainCoroutineRule.kt @@ -0,0 +1,29 @@ +package tw.y_studio.ptt + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import kotlin.coroutines.ContinuationInterceptor + +/** + * Created by Michael.Lien + * on 2021/2/1 + */ +@ExperimentalCoroutinesApi +class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() { + + override fun starting(description: Description?) { + super.starting(description) + Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher) + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + } +} diff --git a/app/src/sharedTest/java/tw/y_studio/ptt/TestJsonFileUtils.kt b/app/src/sharedTest/java/tw/y_studio/ptt/TestJsonFileUtils.kt new file mode 100644 index 0000000..bc9ffa3 --- /dev/null +++ b/app/src/sharedTest/java/tw/y_studio/ptt/TestJsonFileUtils.kt @@ -0,0 +1,27 @@ +package tw.y_studio.ptt + +import java.io.BufferedReader +import java.io.InputStreamReader + +/** + * Created by Michael.Lien + * on 2021/2/1 + */ +object TestJsonFileUtils { + fun loadJsonFile(fileName: String): String { + val classloader = javaClass.classLoader + val inputStream = classloader.getResourceAsStream(fileName) + val builder = StringBuilder() + val buffer = CharArray(1024) + val reader = BufferedReader(InputStreamReader(inputStream)) + var n: Int + while (true) { + n = reader.read(buffer) + if (n < 0) break + builder.append(buffer, 0, n) + } + inputStream.close() + + return builder.toString() + } +} diff --git a/app/src/test/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceTest.kt b/app/src/test/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceTest.kt index 22c7915..1f73756 100644 --- a/app/src/test/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceTest.kt +++ b/app/src/test/java/tw/y_studio/ptt/source/remote/board/PopularRemoteDataSourceTest.kt @@ -1,63 +1,66 @@ package tw.y_studio.ptt.source.remote.board +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth +import com.google.gson.Gson import io.mockk.MockKAnnotations -import io.mockk.every +import io.mockk.coEvery import io.mockk.impl.annotations.MockK -import org.json.JSONException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test -import tw.y_studio.ptt.api.PopularBoardListAPI +import tw.y_studio.ptt.MainCoroutineRule +import tw.y_studio.ptt.TestJsonFileUtils import tw.y_studio.ptt.api.board.BoardApiService -import tw.y_studio.ptt.api.model.hot_board.HotBoardTemp +import tw.y_studio.ptt.api.model.hot_board.HotBoard +import tw.y_studio.ptt.utils.fromJson +@ExperimentalCoroutinesApi class PopularRemoteDataSourceTest { private lateinit var popularRemoteDataSource: IPopularRemoteDataSource - @MockK - private lateinit var popularBoardListAPI: PopularBoardListAPI - @MockK private lateinit var boardApi: BoardApiService + @get:Rule + var mainCoroutineRule = MainCoroutineRule() + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + @Before fun setUp() { MockKAnnotations.init(this) - popularRemoteDataSource = PopularRemoteDataSourceImpl(boardApi, popularBoardListAPI) + popularRemoteDataSource = PopularRemoteDataSourceImpl(boardApi) } @Test - fun get_popular_board_data_then_return_data() { + fun get_popular_board_data_then_return_data() = runBlockingTest { // GIVEN - val data = HotBoardTemp(1, "foo", "bar", 1, 1, "") - every { popularBoardListAPI.refresh(any(), any()) } returns mutableListOf(data) + val jsonFile = TestJsonFileUtils.loadJsonFile("api/board/popular_boards.json") + val hotBoard = Gson().fromJson(jsonFile) + + coEvery { boardApi.getPopularBoard() } returns hotBoard // WHEN - val result = popularRemoteDataSource.getPopularBoardData(1, 10) + val result = popularRemoteDataSource.getPopularBoards() // THEN Truth.assertThat(result).apply { - isNotEmpty() - hasSize(1) - containsExactly(data) + isEqualTo(hotBoard) } } private fun createSampleData() = mapOf("foo" to "bar") @Test(expected = Exception::class) - fun get_popular_board_data_then_throw_exception() { - every { popularBoardListAPI.refresh(any(), any()) } throws Exception() - - popularRemoteDataSource.getPopularBoardData(0, 0) - } - - @Test(expected = JSONException::class) - fun get_popular_board_data_then_throw_json_exception() { - every { popularBoardListAPI.refresh(any(), any()) } throws JSONException("error!") + fun get_popular_board_data_then_throw_exception() = runBlockingTest { + coEvery { boardApi.getPopularBoard() } throws Exception() - popularRemoteDataSource.getPopularBoardData(0, 0) + popularRemoteDataSource.getPopularBoards() } @After diff --git a/app/src/test/resources/api/board/popular_boards.json b/app/src/test/resources/api/board/popular_boards.json new file mode 100644 index 0000000..062f398 --- /dev/null +++ b/app/src/test/resources/api/board/popular_boards.json @@ -0,0 +1,23 @@ +{ + "list": [ + { + "bid": "string", + "brdname": "string", + "title": "string", + "flag": 0, + "type": "string", + "class": "string", + "nuser": 0, + "moderators": [ + "string" + ], + "reason": "string", + "read": true, + "total": 0, + "last_post_time": 0, + "stat_attr": 0, + "level_idx": "string" + } + ], + "next_idx": "string" +} \ No newline at end of file diff --git a/versions.gradle b/versions.gradle index 7ccdbe6..f1f3d50 100644 --- a/versions.gradle +++ b/versions.gradle @@ -1,42 +1,43 @@ ext.buildConfig = [ - 'appID': 'tw.y_studio.ptt', - 'compileSDK': 30, - 'minSDK' : 26, - 'targetSDK' : 30, - 'buildTool' : '30.0.2', + 'appID' : 'tw.y_studio.ptt', + 'compileSDK' : 30, + 'minSDK' : 26, + 'targetSDK' : 30, + 'buildTool' : '30.0.2', 'versionCode': 43, 'versionName': "0.0.14_build2", - 'runner': "androidx.test.runner.AndroidJUnitRunner", - 'jdk': '1.8' + 'runner' : "androidx.test.runner.AndroidJUnitRunner", + 'jdk' : '1.8' ] ext.versions = [ - 'androidGradle' : '4.1.2', - 'koin' : '2.2.0', - 'kotlin' : '1.4.10', - 'androidXAppCompat' : '1.2.0', - 'androidXBrowser' : '1.2.0', - 'androidXConstraintLayout' : '2.0.4', - 'androidXLegacy' : '1.0.0', - 'androidXLifecycle' : '2.2.0', - 'androidXNavigation' : '2.3.1', + 'androidGradle' : '4.1.2', + 'koin' : '2.2.0', + 'kotlin' : '1.4.10', + 'androidXAppCompat' : '1.2.0', + 'androidXArch' : '2.1.0', + 'androidXBrowser' : '1.2.0', + 'androidXConstraintLayout' : '2.0.4', + 'androidXLegacy' : '1.0.0', + 'androidXLifecycle' : '2.2.0', + 'androidXNavigation' : '2.3.1', 'androidXLocalBroadcastManager': '1.0.0', - 'androidXMultidex': '2.0.1', - 'androidXSwipeRefreshLayout': '1.1.0', - 'androidXTextJunit': '1.1.2', - 'androidXTextEspresso': '3.3.0', - 'fresco': '2.3.0', - 'jsoup': '1.13.1', - 'junit': '4.13', - 'material': '1.2.1', - "mockK": '1.10.2', - 'okhttp': '4.9.0', - 'okio': '2.8.0', - 'retrofit': '2.9.0', - 'truth': '1.1', - 'leakcanary': '2.5', - 'gson' : '2.8.6', - 'coroutines' : '1.4.2' + 'androidXMultidex' : '2.0.1', + 'androidXSwipeRefreshLayout' : '1.1.0', + 'androidXTextJunit' : '1.1.2', + 'androidXTextEspresso' : '3.3.0', + 'fresco' : '2.3.0', + 'jsoup' : '1.13.1', + 'junit' : '4.13', + 'material' : '1.2.1', + "mockK" : '1.10.2', + 'okhttp' : '4.9.0', + 'okio' : '2.8.0', + 'retrofit' : '2.9.0', + 'truth' : '1.1', + 'leakcanary' : '2.5', + 'gson' : '2.8.6', + 'coroutines' : '1.4.2' ] ext.projectDeps = [ @@ -48,69 +49,71 @@ ext.projectDeps = [ ext.deps = [ 'kotlin' : [ - 'stdlib': [ + 'stdlib' : [ 'jdk8': "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" ], 'coroutines': [ - 'core': "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}", - 'android': "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}" + 'core' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}", + 'android': "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}", + 'test' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.coroutines}" ] ], - 'koin': [ - 'android': "org.koin:koin-android:${versions.koin}", - 'scope': "org.koin:koin-androidx-scope:${versions.koin}", + 'koin' : [ + 'android' : "org.koin:koin-android:${versions.koin}", + 'scope' : "org.koin:koin-androidx-scope:${versions.koin}", 'viewModel': "org.koin:koin-androidx-viewmodel:${versions.koin}", - 'fragment': "org.koin:koin-androidx-fragment:${versions.koin}" + 'fragment' : "org.koin:koin-androidx-fragment:${versions.koin}" ], 'androidX': [ - 'appcompat' : "androidx.appcompat:appcompat:${versions.androidXAppCompat}", - 'browser': "androidx.browser:browser:${versions.androidXBrowser}", - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:${versions.androidXConstraintLayout}", - 'legacy' : "androidx.legacy:legacy-support-v4:${versions.androidXLegacy}", - 'lifecycle' : [ + 'arch' : "androidx.arch.core:core-testing:${versions.androidXArch}", + 'appcompat' : "androidx.appcompat:appcompat:${versions.androidXAppCompat}", + 'browser' : "androidx.browser:browser:${versions.androidXBrowser}", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:${versions.androidXConstraintLayout}", + 'legacy' : "androidx.legacy:legacy-support-v4:${versions.androidXLegacy}", + 'lifecycle' : [ 'extensions' : "androidx.lifecycle:lifecycle-extensions:${versions.androidXLifecycle}", 'compiler' : "androidx.lifecycle:lifecycle-compiler:${versions.androidXLifecycle}", 'viewmodelKTX': "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidXLifecycle}", - 'runtime' : "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidXLifecycle}" + 'runtime' : "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidXLifecycle}" ], - 'navigation' : [ - 'fragment' : "androidx.navigation:navigation-fragment-ktx:${versions.androidXNavigation}", - 'ui' : "androidx.navigation:navigation-ui-ktx:${versions.androidXNavigation}" + 'navigation' : [ + 'fragment': "androidx.navigation:navigation-fragment-ktx:${versions.androidXNavigation}", + 'ui' : "androidx.navigation:navigation-ui-ktx:${versions.androidXNavigation}" ], 'localBroadcastManager': "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidXLocalBroadcastManager}", - 'multidex': "androidx.multidex:multidex:${versions.androidXMultidex}", - 'swipeRefreshLayout': "androidx.swiperefreshlayout:swiperefreshlayout:${versions.androidXSwipeRefreshLayout}", - 'test': [ + 'multidex' : "androidx.multidex:multidex:${versions.androidXMultidex}", + 'swipeRefreshLayout' : "androidx.swiperefreshlayout:swiperefreshlayout:${versions.androidXSwipeRefreshLayout}", + 'test' : [ 'ext': [ - 'junit': "androidx.test.ext:junit:${versions.androidXTextJunit}", + 'junit' : "androidx.test.ext:junit:${versions.androidXTextJunit}", 'espresso': "androidx.test.espresso:espresso-core:${versions.androidXTextEspresso}" ] ] ], - 'google': [ + 'google' : [ 'material': "com.google.android.material:material:${versions.material}", - 'truth': "com.google.truth:truth:${versions.truth}", - "gson" : "com.google.code.gson:gson:${versions.gson}" + 'truth' : "com.google.truth:truth:${versions.truth}", + "gson" : "com.google.code.gson:gson:${versions.gson}" ], - 'jsoup': "org.jsoup:jsoup:${versions.jsoup}", - 'junit': "junit:junit:${versions.junit}", + 'jsoup' : "org.jsoup:jsoup:${versions.jsoup}", + 'junit' : "junit:junit:${versions.junit}", 'facebook': [ 'fresco': [ - 'core': "com.facebook.fresco:fresco:${versions.fresco}", - 'animate': "com.facebook.fresco:animated-gif:${versions.fresco}", + 'core' : "com.facebook.fresco:fresco:${versions.fresco}", + 'animate' : "com.facebook.fresco:animated-gif:${versions.fresco}", 'imagePipeline': "com.facebook.fresco:imagepipeline-okhttp3:${versions.fresco}" ] ], - "mockK": [ - "core": "io.mockk:mockk:${versions.mockK}", + "mockK" : [ + "core" : "io.mockk:mockk:${versions.mockK}", "android": "io.mockk:mockk-android:${versions.mockK}", ], - 'square': [ - 'okhttp': "com.squareup.okhttp3:okhttp:${versions.okhttp}", - 'okio': "com.squareup.okio:okio:${versions.okio}", + 'square' : [ + 'okhttp' : "com.squareup.okhttp3:okhttp:${versions.okhttp}", + 'okio' : "com.squareup.okio:okio:${versions.okio}", 'leakcanary': "com.squareup.leakcanary:leakcanary-android:${versions.leakcanary}", - 'retrofit': [ - 'core': "com.squareup.retrofit2:retrofit:${versions.retrofit}", + 'retrofit' : [ + 'core' : "com.squareup.retrofit2:retrofit:${versions.retrofit}", 'gsonConverter': "com.squareup.retrofit2:converter-gson:${versions.retrofit}" ] ]