Upgrade to Pro — share decks privately, control downloads, hide ads and more …

既存コードへのテスト追加とリファクタリングの実践

Avatar for makun makun
December 19, 2024

 既存コードへのテスト追加とリファクタリングの実践

Avatar for makun

makun

December 19, 2024
Tweet

More Decks by makun

Other Decks in Programming

Transcript

  1. class TestWorker( private val context: Context, workerParams: WorkerParameters ) :

    CoroutineWorker(context, workerParams) { private suspend fun filterEpisodes(...): Result<List<...>> { … val filteredEpisodes = updatedEpisodes .filter { ... } .filter { ... } .distinctBy { ... } return Result.success(filteredEpisodes) } } WorkManagerの CoroutineWorkerに実装され たfilterEpisodesのロジックを テストする。 テスト対象クラス:TestWorker テスト対象関数 :filterEpisodes
  2. class TestWorkerTest { @Test fun 作品のベルマークがoffの場合はその作品の通知を除く() {} @Test fun ひとつの作品で複数のエピソードの更新があった場合は一番優先度の高いnotificationTypeの通知だけを残す()

    {} @Test fun すでに読了しているエピソードの通知を除く() {} @Test fun ユーザーのコイン通知設定がoffの場合はnotificationTypeがNEW_COINの通知を除く() {} @Test fun ユーザーのチケットで読める話の追加の通知設定がoffの場合はnotificationTypeがNEW_TICKETとCOIN_TO_TICKETの通知を除く() {} @Test fun ひとつの作品で同じnotificationTypeが複数あった場合はエピソードIDが一番大きいものを残す() {} }
  3. class TestWorker( private val context: Context, workerParams: WorkerParameters ) :

    CoroutineWorker(context, workerParams) { private suspend fun filterEpisodes(...): Result<List<...>> { … val filteredEpisodes = updatedEpisodes .filter { ... } .filter { ... } .distinctBy { ... } return Result.success(filteredEpisodes) } } WorkManagerの CoroutineWorkerに実装され たfilterEpisodesのロジックを テストする。 androidx.work:work-testing WorkManagerのWorkerをテ ストするために必要
  4. @Test fun 作品のベルマークがoffの場合はその作品の通知を除く() = runTest { val context = ApplicationProvider.getApplicationContext<Context>()

    val worker = TestListenableWorkerBuilder<TestWorker>(context).build() val inputData = listOf<UpdatedEpisodeCache>() val result = worker.filterEpisodes(inputData) assertTrue(result.isSuccess) }
  5. @Test fun 作品のベルマークがoffの場合はその作品の通知を除く() = runTest { val context = ApplicationProvider.getApplicationContext<Context>()

    val worker = TestListenableWorkerBuilder<TestWorker>(context).build() val inputData = listOf<UpdatedEpisodeCache>() val result = worker.filterEpisodes(inputData) assertTrue(result.isSuccess) }
  6. val inputData = listOf( UpdatedEpisodeCache( episodeId = "12", comicId =

    "7"), UpdatedEpisodeCache(...), ... ) val expectedData = listOf( UpdatedEpisodeCache(...), UpdatedEpisodeCache(...), … ) val result = worker.filterEpisodes(inputData) assertTrue(result.isSuccess)
  7. val inputData = listOf( UpdatedEpisodeCache( episodeId = "12", comicId =

    "7"), UpdatedEpisodeCache(...), ... ) val expectedData = listOf( UpdatedEpisodeCache(...), UpdatedEpisodeCache(...), … ) val result = worker.filterEpisodes(inputData) assertTrue(result.isSuccess)
  8. val inputData = listOf(...) val expectedData = listOf(...) val result

    = worker.filterEpisodes(inputData) // assertTrue(result.isSuccess) assertContentEquals( expected = expectedData, actual = result.getOrThrow() )
  9. class TestWorkerTest : KoinTest { @BeforeTest fun setup() { startKoin

    {} } @AfterTest fun tearDown() { stopKoin() } } Koinの公式ドキュメントに従っ てテストコードに KoinTest, startKoin, stopKoin を追記
  10. val filteredEpisodes = updatedEpisodes .filter { episode -> try {

    notificationRepository.isComicNotificationEnabled(episode.comicId) } catch (e: Exception) { return kotlin.Result.failure(e) } }
  11. object : NotificationRepository by mock() { override suspend fun isComicNotificationEnabled(comicId:

    String): Boolean { return false } } isComicNotificationのStubを作成
  12. val filteredEpisodes = updatedEpisodes .filter { episode -> try {

    // ここで常にfalseを返している状態 notificationRepository.isComicNotificationEnabled(episode.comicId) } catch (e: Exception) { return kotlin.Result.failure(e) }
  13. val inputData = listOf( UpdatedEpisodeCache(), UpdatedEpisodeCache( episodeId = "32", comicId

    = "8", ), UpdatedEpisodeCache( episodeId = "28", comicId = "9", ), ... ) val expectedData = listOf( UpdatedEpisodeCache(...), UpdatedEpisodeCache( episodeId = "28", comicId = "9", ), … comicId = “8” のデータは無い comicId = “8”のデータは ベルマークがoffのため 通知対象から除かれている テストで利用するデータの前提条件
  14. object : NotificationRepository by mock() { override suspend fun isComicNotificationEnabled(comicId:

    String): Boolean { return true } } comicId に関わらず true 前提条件を完全に無視
  15. object : NotificationRepository by mock() { override suspend fun isComicNotificationEnabled(comicId:

    String): Boolean { return when (comicId) { “8”, “10” -> false else -> true } } } 作品のベルマークの状態 についての データを返す関数 予め知っていれば テストの作成が楽になる
  16. interface NotificationRepository { /** * 対象作品の通知設定の状態を返す * * @param comicId

    対象の作品ID * @return 対象作品の通知がONの場合はtrue、OFFの場合はfalse */ suspend fun isComicNotificationEnabled(comicId: String): Boolean KDoc形式で説明されている
  17. .filter { episode -> try { val timestamp = comicsRepository.getFinishReadEpisodeTimestamp(

    comicId = episode.comicId, episodeId = episode.episodeId, ) if (timestamp == null) true else timestamp < currentTime } catch (e: Exception) { return kotlin.Result.failure(e) } }
  18. single<ComicsRepository> { object : ComicsRepository by mock() { override suspend

    fun getFinishReadEpisodeTimestamp( comicId: String, episodeId: String ): Long? {...}