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

テストコードはもう書かない:JetBrains AI Assistantに委ねる非同期処理のテ...

Avatar for makun makun
September 08, 2025

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

Avatar for makun

makun

September 08, 2025
Tweet

More Decks by makun

Other Decks in Programming

Transcript

  1. アジェンダ 1. Androidテストの「つらみ」 2. JetBrains AI Assistant登場 3. Room(DB)のテストをAIに丸投げしてみる 4.

    WorkManagerのテストという壁に挑む 5. AIテスト設計・生成の限界と「うまい付き合い方」 6. AIテスト設計・生成の可能性
  2. // 例: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の関数の例
  3. JetBrains AI Assistant • IntelliJ IDEAやAndroid StudioなどのJetBrains製IDEに統合 できるAIアシスタント • 自然言語の指示をチャット形式で投げると、AIがコードの文脈を理

    解しテストコードやリファクタ案を提案 開発者の“ペアプログラマー ”として、日々の定型作業や調査・設計を 強力にサポート
  4. @Dao interface TaskDao { @Query("SELECT * FROM tasks") fun getTasks():

    Flow<List<Task>> @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
  5. @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
  6. @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
  7. @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
  8. @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
  9. // 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
  10. // 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
  11. // 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の 実装に依存
  12. // 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
  13. @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 エミュレータや 実機デバイスに依存
  14. // 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 連続操作の順序やタイミン グの厳密な検証で再現性 が揺らぐ
  15. @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の生成処理が 重複しやすい
  16. 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
  17. @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
  18. @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
  19. @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
  20. @Test fun givenApiReturnsTasks_... = runTest { // Given ... val

    worker = TestListenableWorkerBuilder<SyncWorker>(context) .setWorkerFactory { appContext, params -> SyncWorker( appContext = appContext, params = params, api = taskApi, dao = taskDao ) } .build() ... } SyncWorkerTest.kt
  21. @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
  22. @Test fun givenApiReturnsTasks_... = runTest { // Given ... val

    worker = TestListenableWorkerBuilder<SyncWorker>(context) .setWorkerFactory { appContext, params -> SyncWorker( appContext = appContext, params = params, api = taskApi, dao = taskDao ) } .build() ... } SyncWorkerTest.kt WorkerFactoryの 継承クラスを渡す