0

I am almost new to android testing and following the official docs and Udacity course for learning purposes.

Coming to the issue I want to check when the task is completed or incompleted to be displayed properly or not, for this I wrote a few tests. Here I got the exception that toast can not be displayed on a thread that has not called Looper.prepare.

When I comment out the toast msg live data updating line of code then all tests work fine and pass successfully. I am new to android testing and searched out a lot but did not get any info to solve this issue. Any help would be much appreciated. A little bit of explanation will be much more helpful if provided.

Below is my test class source code along with ViewModel, FakeRepository, and fragment source code.

Test Class.

@ExperimentalCoroutinesApi
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    @get:Rule
    val rule = InstantTaskExecutorRule()

    private lateinit var tasksRepository: FakeTasksRepository

    @Before
    fun setUp() {
        tasksRepository = FakeTasksRepository()
        ServiceLocator.taskRepositories = tasksRepository
    }

    
    @Test
    fun addNewTask_addNewTaskToDatabase() = mainCoroutineRule.runBlockingTest {
        val newTask = Task(id = "1", userId = 0, title = "Hello AndroidX World",false)
        tasksRepository.addTasks(newTask)
        val task = tasksRepository.getTask(newTask.id)
        assertEquals(newTask.id,(task as Result.Success).data.id)
    }


    @Test
    fun activeTaskDetails_DisplayedInUi() = mainCoroutineRule.runBlockingTest {

        val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",false)
        tasksRepository.addTasks(newTask)
        val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.Theme_ToDoWithTDD)

        onView(withId(R.id.title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))

        onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.complete_checkbox)).check(matches(isNotChecked()))

    }


    @Test
    fun completedTaskDetails_DisplayedInUI() = mainCoroutineRule.runBlockingTest {
        val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",true)
        tasksRepository.addTasks(newTask)

        val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
        launchFragmentInContainer <TaskDetailFragment>(bundle,R.style.Theme_ToDoWithTDD)

        onView(withId(R.id.title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))

        onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.complete_checkbox)).check(matches(isChecked()))
    }

    @After
    fun tearDown() = mainCoroutineRule.runBlockingTest {
        ServiceLocator.resetRepository()
    }
}

FakeRepository class.

class FakeTasksRepository: TasksRepository {

    var tasksServiceData: LinkedHashMap<String,Task> = LinkedHashMap()
    private val observableTasks: MutableLiveData<Result<List<Task>>> = MutableLiveData()

    private var shouldReturnError: Boolean = false

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { fetchAllToDoTasks() }
        return observableTasks.map { tasks ->
            when(tasks) {
                is Result.Loading -> Result.Loading
                is Result.Error -> Result.Error(tasks.exception)
                is Result.Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Result.Error(Exception("Not found"))
                    Result.Success(task)
                }
            }
        }
    }

    override suspend fun completeTask(id: String) {
        tasksServiceData[id]?.completed = true
    }

    override suspend fun completeTask(task: Task) {
        val compTask = task.copy(completed = true)
        tasksServiceData[task.id] = compTask
        fetchAllToDoTasks()
    }

    override suspend fun activateTask(id: String) {
        tasksServiceData[id]?.completed = false
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = task.copy(completed = false)
        tasksServiceData[task.id] = activeTask
        fetchAllToDoTasks()
    }

    override suspend fun getTask(taskId: String): Result<Task> {
        if (shouldReturnError) return Result.Error(Exception("Test Exception"))
        tasksServiceData[taskId]?.let {
            return Result.Success(it)
        }
        return Result.Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun clearAllCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.completed
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        fetchAllToDoTasks()
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        fetchAllToDoTasks()
    }

    override suspend fun fetchAllToDoTasks(): Result<List<Task>> {
        if(shouldReturnError) {
            return Result.Error(Exception("Could not find task"))
        }

        val tasks = Result.Success(tasksServiceData.values.toList())
        observableTasks.value = tasks
        return tasks
    }

    override suspend fun updateLocalDataStore(list: List<Task>) {
        TODO("Not yet implemented")
    }

    fun addTasks(vararg tasks: Task) {
        tasks.forEach {
            tasksServiceData[it.id] = it
        }

        runBlocking {
            fetchAllToDoTasks()
        }
    }
}

Fragment class.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.loadTaskById(args.taskId)

        setUpToast(this,viewModel.toastText)

        viewModel.editTaskEvent.observe(viewLifecycleOwner, {
            it?.let {
                val action = TaskDetailFragmentDirections
                    .actionTaskDetailFragmentToAddEditFragment(
                      args.taskId,
                        resources.getString(R.string.edit_task)
                    )
                findNavController().navigate(action)
            }
        })

        binding.editTaskFab.setOnClickListener {
            viewModel.editTask()
        }

    }

ViewModel class.

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() {

    private val TAG = "TaskDetailViewModel"

    private val _taskId: MutableLiveData<String> = MutableLiveData()
    private val _task = _taskId.switchMap {
        tasksRepository.observeTask(it).map { res ->
            Log.d("Test","res with value ${res.toString()}")
            isolateTask(res)
        }
    }

    val task: LiveData<Task?> = _task
    private val _toastText = MutableLiveData<Int?>()
    val toastText: LiveData<Int?> = _toastText

    private val _dataLoading = MutableLiveData<Boolean>()
    val dataLoading: LiveData<Boolean> = _dataLoading
    private val _editTaskEvent = MutableLiveData<Unit?>(null)
    val editTaskEvent: LiveData<Unit?> = _editTaskEvent

    fun loadTaskById(taskId: String) {
        if(dataLoading.value == true || _taskId.value == taskId) return

        _taskId.value = taskId
        Log.d("Test","loading task with id $taskId")
    }

    fun editTask(){
        _editTaskEvent.value = Unit
    }

    fun setCompleted(completed: Boolean) = viewModelScope.launch {
        val task = _task.value ?: return@launch
        if(completed) {
            tasksRepository.completeTask(task.id)
            _toastText.value = R.string.task_marked_complete
        }
         else {
            tasksRepository.activateTask(task.id)
            _toastText.value = R.string.task_marked_active
        }
    }

    private fun isolateTask(result: Result<Task?>): Task? {
        return if(result is Result.Success) {
            result.data
        } else {
            _toastText.value = R.string.loading_tasks_error
            null
        }
    }


    @Suppress("UNCHECKED_CAST")
    class TasksDetailViewModelFactory(
        private val tasksRepository: TasksRepository
    ): ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return (TaskDetailViewModel(
                tasksRepository
            ) as T)
        }
    }

}

In this method in ViewModel when I comment out the below line of code all tests passed.

_toastText.value = R.string.loading_tasks_error

private fun isolateTask(result: Result<Task?>): Task? {
        return if(result is Result.Success) {
            result.data
        } else {
            _toastText.value = R.string.loading_tasks_error // Comment out this line then all test passed.
            null
        }
    }
Wisal
  • 153
  • 2
  • 10
  • The error means that you are trying to show a Toast message from a non-UI thread. This should answer your question: https://stackoverflow.com/a/3875204/7477675 Edit: This question also has answers that might be helpful: https://stackoverflow.com/questions/47536005/cant-toast-on-a-thread-that-has-not-called-looper-prepare?rq=1 – anshajkhare Dec 20 '21 at 12:43
  • @anshajkhare thank you for your time and interest I see I have to call toast on the main thread but here my question is, is there any other form test class to solve this main thread issue. – Wisal Dec 20 '21 at 12:59
  • Here i am calling the toast msg with the main thread but in testing, i am using a coroutine dispatcher which I think causes the issue here. – Wisal Dec 20 '21 at 13:56

0 Answers0