Slide 1

Slide 1 text

DroidKaigi 2025 テストコードはもう書かない:JetBrains AI Assistantに委ねる非同期処理のテ スト自動設計・生成 Masahiro Saito English 日本語

Slide 2

Slide 2 text

Masahiro Saito 齊藤 正宏 ピクシブ株式会社 8年目 スマホアプリエンジニア 兼エンジニア採用リーダー github.com/m4kvn

Slide 3

Slide 3 text

アジェンダ 1. Androidテストの「つらみ」 2. JetBrains AI Assistant登場 3. Room(DB)のテストをAIに丸投げしてみる 4. WorkManagerのテストという壁に挑む 5. AIテスト設計・生成の限界と「うまい付き合い方」 6. AIテスト設計・生成の可能性

Slide 4

Slide 4 text

Androidテストの「つらみ」 The "pains" of Android testing

Slide 5

Slide 5 text

Androidテストの理想 「品質はテストで担保!バグゼロでリリースするぞ!」 ● 高いテストカバレッジ ● 仕様変更に強い堅牢なテスト ● 誰でも書ける分かりやすいテストコード

Slide 6

Slide 6 text

Androidテストの現実 「うっ、時間がない...テストは後で...」 ● 非同期処理(Coroutine, Flow)が絡むと一気に複雑に ● DI、DB、API通信...モックの準備がとにかく面倒 ● テストコードが属人化し、メンテナンスされない 結論:テストを書くのは、時間も手間もかかる大変な作業

Slide 7

Slide 7 text

なぜ非同期処理のテストは難しい? Why is asynchronous testing difficult?

Slide 8

Slide 8 text

// 例:ViewModelの一部 fun fetchData() { viewModelScope.launch { _uiState.value = Loading try { val data = repository.fetchData() // suspend fun val processedData = process(data) _uiState.value = Success(processedData) } catch (e: Exception) { _uiState.value = Error(e) } } } 一般的なデータ取得ロジックをもつViewModelの関数の例

Slide 9

Slide 9 text

非同期処理のテストはお作法だらけ ● スレッドの差し替え ● 非同期値の監視 ● 依存のモック化 これらの「お作法」を毎回手作業で書くのは大変 「この面倒な定型作業こそ、 AIに任せるべきではないか?

Slide 10

Slide 10 text

JetBrains AI Assistant登場 Introducing JetBrains AI Assistant

Slide 11

Slide 11 text

JetBrains AI Assistant ● IntelliJ IDEAやAndroid StudioなどのJetBrains製IDEに統合 できるAIアシスタント ● 自然言語の指示をチャット形式で投げると、AIがコードの文脈を理 解しテストコードやリファクタ案を提案 開発者の“ペアプログラマー ”として、日々の定型作業や調査・設計を 強力にサポート

Slide 12

Slide 12 text

JetBrains AI Assistant

Slide 13

Slide 13 text

Gemini ● Android StudioではGeminiが利用できる ● GeminiとJetBrains AI Assistantは別物 ● 本セッションで違いについては言及しない

Slide 14

Slide 14 text

重視する評価軸 正確性:仕様に対して正しい実装か、偽陽性/偽陰性が低いか 網羅性:命令網羅と分岐網羅、重要パスは充足か 再現性:CIなどローカル以外での安定性は高いか 保守性:意図が伝わるか、仕様変更時の追従コストが低いか 速度/コスト:CI実行時間やレビュー時間などのコストが低いか これらの評価軸を元に「AIに任せるべきか」を判断

Slide 15

Slide 15 text

【実践1】Room(DB)のテストをAIに丸投 げしてみる [Practice 1] Let's try to outsource the Room (DB) testing to AI

Slide 16

Slide 16 text

お題: TaskというエンティティをCRUDする TaskDaoのテストを生成する

Slide 17

Slide 17 text

@Dao interface TaskDao { @Query("SELECT * FROM tasks") fun getTasks(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTask(task: Task) @Update suspend fun updateTask(task: Task) @Query("DELETE FROM tasks WHERE id = :taskId") suspend fun deleteTaskById(taskId: String) } TaskDao.kt

Slide 18

Slide 18 text

TaskDaoのためのユニットテストを作成してください。 作成するテストは次の要件を満たして下さい。 ・in-memory databaseを使用する ・@Beforeでデータベースを初期化し@Afterで閉じる ・テストはkotlinx-coroutines-testのrunTestを使用する テストケースは以下を網羅してください。 ・insertTaskで挿入したタスクがgetTasksで正しく取得できる ・updateTaskで更新した内容が正しく反映される ・deleteTaskByIdで削除したタスクがリストから消える TaskDaoのテストを生成させるためのプロンプト

Slide 19

Slide 19 text

【実践1】AIが生成したコードの注目すべ きポイント [Practice 1] Important points about AI-generated code

Slide 20

Slide 20 text

@RunWith(AndroidJUnit4::class) class TaskDaoTest { private lateinit var database: TaskDatabase private lateinit var taskDao: TaskDao @Before fun setup() { database = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), TaskDatabase::class.java ).allowMainThreadQueries().build() taskDao = database.taskDao() } @After fun teardown() { database.close() } TaskDaoTest.kt

Slide 21

Slide 21 text

@Test fun insertTask_getsTasks_returnsInsertedTask() = runTest { // Given val task = Task(id = "task1", title = "Test Task") // When taskDao.insertTask(task) val tasks = taskDao.getTasks().first() // Then assertEquals(1, tasks.size) assertEquals(task, tasks[0]) } TaskDaoTest.kt

Slide 22

Slide 22 text

@Test fun insertTask_getsTasks_returnsInsertedTask() @Test fun updateTask_getsTasks_returnsUpdatedTask() @Test fun deleteTaskById_getsTasks_returnsEmptyList() @Test fun multipleInsertTasks_getsTasks_returnsAllTasks() @Test fun insertDuplicateTaskId_getsTasks_replacesOldTask() TaskDaoTest.kt

Slide 23

Slide 23 text

@Test fun multipleInsertTasks_getsTasks_returnsAllTasks() = runTest { ... // Then assertEquals(3, tasks.size) assertTrue(tasks.contains(task1)) assertTrue(tasks.contains(task2)) assertTrue(tasks.contains(task3)) } TaskDaoTest.kt

Slide 24

Slide 24 text

【実践1】AI生成コードの注目ポイント ● 「セットアップ→操作→検証」という流れが明確 ● 基本的なCRUDの正常系動作をカバー

Slide 25

Slide 25 text

【実践1】評価軸による分析 [Practice 1] Analysis based on evaluation criteria

Slide 26

Slide 26 text

正確性の評価(Good) ● 基本的なCRUD操作の正常系を明確に検証できている

Slide 27

Slide 27 text

// Given val task1 = Task(id = "task1", title = "Task 1") val task2 = Task(id = "task2", title = "Task 2") val task3 = Task(id = "task3", title = "Task 3") // When taskDao.insertTask(task1) taskDao.insertTask(task2) taskDao.insertTask(task3) val tasks = taskDao.getTasks().first() // Then assertEquals(3, tasks.size) assertTrue(tasks.contains(task1)) assertTrue(tasks.contains(task2)) assertTrue(tasks.contains(task3)) TaskDaoTest.kt

Slide 28

Slide 28 text

// Given val task1 = Task(id = "same_id", title = "Old Task") val task2 = Task(id = "same_id", title = "New Task") // When taskDao.insertTask(task1) taskDao.insertTask(task2) // Overwrite with same ID val tasks = taskDao.getTasks().first() // Then assertEquals(1, tasks.size) assertEquals(task2, tasks[0]) assertEquals("New Task", tasks[0].title) TaskDaoTest.kt

Slide 29

Slide 29 text

正確性の評価(Good) ● 基本的なCRUD操作の正常系を明確に検証できている ● @Before/@Afterによるセットアップ・クリーンアップでテスト間の副 作用を排除できている ● runTestを用いて非同期処理も正確に検証できている

Slide 30

Slide 30 text

// Given val task1 = Task(id = "task1", title = "Task 1") val task2 = Task(id = "task2", title = "Task 2") val task3 = Task(id = "task3", title = "Task 3") // When taskDao.insertTask(task1) taskDao.insertTask(task2) taskDao.insertTask(task3) val tasks = taskDao.getTasks().first() // Then assertEquals(3, tasks.size) assertTrue(tasks.contains(task1)) assertTrue(tasks.contains(task2)) assertTrue(tasks.contains(task3)) TaskDaoTest.kt equals/hashCodeの 実装に依存

Slide 31

Slide 31 text

正確性の評価(Bad) ● 削除や更新のテストにおいて、フィールド単位のアサーションや件数 ・副作用の明示的検証がないため偽陽性が残る ● 例外系や制約違反時の動作(必須項目null等)は未カバー

Slide 32

Slide 32 text

正確性の評価のまとめ ● 「基本的なCRUDの正常系」に関しては十分な正確性がある ● より厳密な検証や異常系のカバーが必要 ○ フィールド単位でのアサーション ○ 件数・副作用の明示的な検証

Slide 33

Slide 33 text

網羅性の評価(Good) ● 基本的なCRUDの正常系(命令網羅)は十分にカバー ○ タスクの新規挿入や複数件の取得 ○ 同一IDでのREPLACE(上書き)動作 ● 日常的によく使われる操作が期待通りに動作するかを確認するテス トケースが揃っている

Slide 34

Slide 34 text

// Given val task1 = Task(id = "task1", title = "Task 1") val task2 = Task(id = "task2", title = "Task 2") val task3 = Task(id = "task3", title = "Task 3") // When taskDao.insertTask(task1) taskDao.insertTask(task2) taskDao.insertTask(task3) val tasks = taskDao.getTasks().first() // Then assertEquals(3, tasks.size) assertTrue(tasks.contains(task1)) assertTrue(tasks.contains(task2)) assertTrue(tasks.contains(task3)) TaskDaoTest.kt

Slide 35

Slide 35 text

未検証の分岐や境界ケース ● 空のDBからの取得 ● 存在しないIDに対するupdateやdeleteの動作 ● 複数件の挿入時における取得順序の保証 ● insert→update→deleteといった連続操作時のFlowのイベント順序や内 容の検証 ● 必須項目がnullの場合や制約違反時の例外発生など、異常系の動作

Slide 36

Slide 36 text

網羅性の評価のまとめ ● 基本的なCRUDの正常系(命令網羅)は十分にカバーされていて、 日常利用には十分 ● 堅牢性向上には分岐や異常系の追加検証が必要

Slide 37

Slide 37 text

再現性の評価(Good) ● in-memory DatabaseとrunTestの利用で外部要因を最小化し、 基本的な再現性は確保されている ● 同じ入力・状態であれば同じ結果が得られる

Slide 38

Slide 38 text

@RunWith(AndroidJUnit4::class) class TaskDaoTest { private lateinit var database: TaskDatabase private lateinit var taskDao: TaskDao @Before fun setup() { database = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), TaskDatabase::class.java ).allowMainThreadQueries().build() taskDao = database.taskDao() } TaskDaoTest.kt エミュレータや 実機デバイスに依存

Slide 39

Slide 39 text

// Given val task1 = Task(id = "task1", title = "Task 1") val task2 = Task(id = "task2", title = "Task 2") val task3 = Task(id = "task3", title = "Task 3") // When taskDao.insertTask(task1) taskDao.insertTask(task2) taskDao.insertTask(task3) val tasks = taskDao.getTasks().first() // Then assertEquals(3, tasks.size) assertTrue(tasks.contains(task1)) assertTrue(tasks.contains(task2)) assertTrue(tasks.contains(task3)) TaskDaoTest.kt 連続操作の順序やタイミン グの厳密な検証で再現性 が揺らぐ

Slide 40

Slide 40 text

再現性の評価のまとめ ● 「基本的なCRUDの単発検証」においては十分な再現性を持つ ● 複雑な分岐や連続イベント、環境依存の排除まで求める場合は、テ ストランナーや検証手法の見直しなど、追加の工夫が必要

Slide 41

Slide 41 text

保守性の評価(Good) ● テストコードはシンプルで読みやすく、責務分離も明確 ● @Before/@Afterによるセットアップ・クリーンアップが適切で副作 用が少ない ● テストケース命名はGWT形式で意図が分かりやすい

Slide 42

Slide 42 text

@RunWith(AndroidJUnit4::class) class TaskDaoTest { private lateinit var database: TaskDatabase private lateinit var taskDao: TaskDao @Before fun setup() { database = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), TaskDatabase::class.java ).allowMainThreadQueries().build() taskDao = database.taskDao() } TaskDaoTest.kt DB/DAOの生成処理が 重複しやすい

Slide 43

Slide 43 text

保守性の評価(Bad) ● 取得順序の前提やFlowの検証方法などの標準化がされていない ため、今後テスト方針がばらつく可能性がある ● DAOにORDER BYを明記して順序の前提を明確化させたり、Flow の検証はTurbineで統一するなど、プロジェクト内でのテストスタイ ルを標準化したい

Slide 44

Slide 44 text

保守性の評価のまとめ ● 保守性の観点で一定の水準を満たしている ● テストケースの増加や仕様変更に備えて、共通化・標準化などを進 めることで、より高い保守性を実現できる

Slide 45

Slide 45 text

速度/コストの評価(Good) ● in-memory DB利用でテスト実行が高速 ● 1ケースあたりの記述・実行コストが低い ● AI生成で初期作成コストも小さい

Slide 46

Slide 46 text

速度/コストの評価(Bad) ● テストケース増加時にコードの重複がコスト増加の要因となる可能 性がある ● 端末に依存するテストランナーの利用は、実行時間やCIコストが大 きくなる可能性がある ● Robolectricやパラメタライズテスト、共通化の工夫で、速度とコス トのバランスを維持する必要がある

Slide 47

Slide 47 text

速度/コストの評価のまとめ ● 非常に効率的であり、現状の規模や内容であれば開発・CIの負担 も小さい ● 今後の拡張や複雑化に備えて、共通化や効率化の工夫を取り入れ る必要はあり

Slide 48

Slide 48 text

CRUD中心のテスト生成を「全て」 AIに任せてしまって良いか? 基本的な正常系・定型パターンに限ればAIに任せて問題ない が、現状では我々による補完やレビューも依然として必要

Slide 49

Slide 49 text

【実践2】WorkManagerのテストという 壁に挑む [Practice 2] Tackling the challenge of testing WorkManager

Slide 50

Slide 50 text

お題: APIからデータを取得しDBに保存する SyncWorkerのテストを生成する

Slide 51

Slide 51 text

class SyncWorker( appContext: Context, params: WorkerParameters, private val api: TaskApi, // RetrofitのAPI private val dao: TaskDao // RoomのDAO ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { return try { val tasks = api.fetchTasks() // ネットワーク通信 tasks.forEach { dao.insertTask(it) } // DB書き込み Result.success() } catch (e: Exception) { Result.failure() } } } SyncWorker.kt

Slide 52

Slide 52 text

SyncWorkerのユニットテストを作成してください。 作成するテストは次の要件を満たして下さい。 ・TestListenableWorkerBuilderを使用する ・依存しているTaskApiとTaskDaoはMockKを使ってモック化する テストケースは以下を網羅してください。 成功ケースのテスト ・api.fetchTasks()がタスクリストを返却した場合、Result.successを返す ・dao.insertTask()が呼ばれている 失敗ケースのテスト ・api.fetchTasks()が例外を投げた場合、Result.failure()を返す SyncWorkerのテストを生成させるためのプロンプト

Slide 53

Slide 53 text

【実践2】AIが生成したコードの注目すべ きポイント [Practice 2] Important points about AI-generated code

Slide 54

Slide 54 text

@RunWith(RobolectricTestRunner::class) class SyncWorkerTest { private lateinit var context: Context private lateinit var taskApi: TaskApi private lateinit var taskDao: TaskDao @Before fun setUp() { context = ApplicationProvider.getApplicationContext() taskApi = mockk() taskDao = mockk(relaxUnitFun = true) } SyncWorkerTest.kt

Slide 55

Slide 55 text

@Test fun givenApiReturnsTasks...() = runTest { // Given val tasks = listOf(...) coEvery { taskApi.fetchTasks() } returns tasks val worker = ... // When val result = worker.doWork() // Then assertEquals(ListenableWorker.Result.success(), result) tasks.forEach { task -> coVerify { taskDao.insertTask(task) } } } SyncWorkerTest.kt

Slide 56

Slide 56 text

【実践2】AI生成コードの注目ポイント ● 「セットアップ→操作→検証」という流れが明確 ● 主要な分岐(成功・失敗)をしっかりカバー ● MockKやRobolectric、TestListenableWorkerBuilder といったテスト支援ツールを適切に活用

Slide 57

Slide 57 text

【実践2】評価軸による分析 [Practice 2] Analysis based on evaluation criteria

Slide 58

Slide 58 text

正確性の評価

Slide 59

Slide 59 text

@RunWith(RobolectricTestRunner::class) class SyncWorkerTest { private lateinit var context: Context private lateinit var taskApi: TaskApi private lateinit var taskDao: TaskDao @Before fun setUp() { context = ApplicationProvider.getApplicationContext() taskApi = mockk() taskDao = mockk(relaxUnitFun = true) } SyncWorkerTest.kt

Slide 60

Slide 60 text

@Test fun givenApiReturnsTasks_... = runTest { // Given ... val worker = TestListenableWorkerBuilder(context) .setWorkerFactory { appContext, params -> SyncWorker( appContext = appContext, params = params, api = taskApi, dao = taskDao ) } .build() ... } SyncWorkerTest.kt

Slide 61

Slide 61 text

@Test fun givenApiReturnsTasks_whenWorkerExecutes_thenRetur nsSuccessAndSavesTasks() @Test fun givenApiThrowsException_whenWorkerExecutes_thenRe turnsFailureAndNoTasksAreSaved() SyncWorkerTest.kt

Slide 62

Slide 62 text

@Test fun givenApiThrowsException_...() = runTest { // Given coEvery { taskApi.fetchTasks() } throws Exception("...") // When val worker = ... val result = worker.doWork() // Then assertEquals(ListenableWorker.Result.failure(), result) coVerify(exactly = 0) { taskDao.insertTask(any()) } } SyncWorkerTest.kt

Slide 63

Slide 63 text

@Test fun givenApiReturnsTasks_... = runTest { // Given ... val worker = TestListenableWorkerBuilder(context) .setWorkerFactory { appContext, params -> SyncWorker( appContext = appContext, params = params, api = taskApi, dao = taskDao ) } .build() ... } SyncWorkerTest.kt WorkerFactoryの 継承クラスを渡す

Slide 64

Slide 64 text

正確性の評価 ● 主要な分岐(成功・失敗)を正確に検証している ● MockKやrunTestなど非同期・モックの使い方も正しい ● setWorkerFactoryの使い方に誤りがあり、修正が必要 十分な正確性はあるが、細かな APIの使い方には配慮が必要

Slide 65

Slide 65 text

網羅性の評価 ● 成功・失敗の主要な分岐をカバーし、基本的な網羅性は十分 ● 境界値や異常系、複数回実行などが未検証 ● 呼び出し回数や個別検証なども未検証 将来的な仕様変更やバグ検出力を高めるには、さらなるケース追加 が必要

Slide 66

Slide 66 text

再現性の評価 ● RobolectricやMockKで外部依存を排除し、安定したテスト実行が 可能 ● 実装の間違いや誤った使い方は、意図しない挙動を発生させるリス クがある 基本的な再現性は担保されているが、実装方法に注意が必要

Slide 67

Slide 67 text

保守性の評価 ● GWT形式の命名・構成で意図が伝わりやすい ● セットアップの共通化で重複が少ない ● WorkerFactoryの共通化やテストデータビルダー導入でさらに保 守性向上

Slide 68

Slide 68 text

速度/コストの評価 ● ローカルテスト・モック活用で実行速度が非常に速い ● 記述量・実行コストも低く、CI負荷も小さい ● 今後の拡張時は共通化や効率化の工夫が有効

Slide 69

Slide 69 text

WorkManager/CoroutineWorkerの テスト生成を「全て」AIに任せてしまって 良いか? 基本的な正常系・主要な分岐・ベストプラクティスに沿った雛 形生成であればAIに十分任せられるが、現状では細かな実 装差異や拡張性・保守性の観点で我々による補完やレビュー が依然として不可欠

Slide 70

Slide 70 text

AI設計の限界と「うまい付き合い方」 The limits of AI design and how to live with it

Slide 71

Slide 71 text

AIによるテスト設計の限界 ● RoomのCRUDテストのような単純なケースはAI生成で十分な品質 が得られる ● WorkManagerなど依存注入や非同期処理が絡む複雑なテストで はAI生成コードに誤りや保守性の課題が残る ● AIは指示した範囲の雛形生成は得意だが、境界値や異常系の網 羅、設計意図の明確化は苦手

Slide 72

Slide 72 text

テスト設計・生成におけるAIの限界 「定型的な部分や雛形生成」はAIに任せて大幅な効率化ができ る一方で、「品質担保・拡張性・設計意図の明確化」といった本質 的な部分は、やはり我々の設計・レビュー・補完が不可欠

Slide 73

Slide 73 text

AIテスト設計・生成の期待と展望 Prospects and Outlook for AI Test Design and Generation

Slide 74

Slide 74 text

現時点でのAIテスト設計・生成の実感 ● AIは雛形生成や定型的なCRUDテストの自動化が得意である ● 複雑な依存関係や非同期処理、境界値・異常系の網羅はAIだけで は難しい

Slide 75

Slide 75 text

AIテスト設計・生成の期待 ● テスト設計意図や仕様の曖昧さをAIが理解できるようになることが今 後の進化の鍵 ● 過去のバグ履歴や失敗パターンを学習し、最適なテスト設計を提案 できる未来が期待される ● テストコードの自動修正や仕様変更への自動追従も将来的な可能 性として挙げられる ● AIがテストの根拠やカバー範囲を説明できるようになることで信頼性 が高まる

Slide 76

Slide 76 text

AIテスト設計・生成の進化の可能性 「単なる雛形生成」から「設計意図や品質担保まで踏み込んだ自 律的なテスト設計」へと進化していく可能性がある。そのために、 AIと我々が対話しながら、お互いの強みを活かしてテスト設計を 進めていく“協調型”のアプローチがますます重要になってくる

Slide 77

Slide 77 text

最後に ● 今はまだ「AI+我々の役割分担」が現実的だが、将来的には「AIが 自律的にテスト設計をリードし、我々がレビューや最終判断を行う」 ような未来も十分にあり得る ● AIの進化をうまく活用しながら、より高品質で効率的なテスト設計を 目指していきたい