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

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

makun
December 19, 2024

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

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? {...}