Skip to content

Commit 6917a14

Browse files
committed
Fixes coroutine and LiveData issues in unit tests
Change-Id: I9ccca05d26c0e218c8b38bc9bfbff62e6b385351
1 parent f4128dd commit 6917a14

File tree

7 files changed

+131
-96
lines changed

7 files changed

+131
-96
lines changed

app/src/test/java/com/example/android/architecture/blueprints/todoapp/LiveDataTestUtil.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package com.example.android.architecture.blueprints.todoapp
1817

1918
import androidx.annotation.VisibleForTesting
@@ -23,26 +22,50 @@ import java.util.concurrent.CountDownLatch
2322
import java.util.concurrent.TimeUnit
2423
import java.util.concurrent.TimeoutException
2524

25+
/**
26+
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
27+
*
28+
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
29+
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
30+
*/
2631
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
27-
fun <T> LiveData<T>.awaitNextValue(
32+
fun <T> LiveData<T>.getOrAwaitValue(
2833
time: Long = 2,
29-
timeUnit: TimeUnit = TimeUnit.SECONDS
34+
timeUnit: TimeUnit = TimeUnit.SECONDS,
35+
afterObserve: () -> Unit = {}
3036
): T {
3137
var data: T? = null
3238
val latch = CountDownLatch(1)
3339
val observer = object : Observer<T> {
3440
override fun onChanged(o: T?) {
3541
data = o
3642
latch.countDown()
37-
this@awaitNextValue.removeObserver(this)
43+
this@getOrAwaitValue.removeObserver(this)
3844
}
3945
}
4046
this.observeForever(observer)
47+
48+
afterObserve.invoke()
49+
4150
// Don't wait indefinitely if the LiveData is not set.
4251
if (!latch.await(time, timeUnit)) {
52+
this.removeObserver(observer)
4353
throw TimeoutException("LiveData value was never set.")
4454
}
4555

4656
@Suppress("UNCHECKED_CAST")
4757
return data as T
4858
}
59+
60+
/**
61+
* Observes a [LiveData] until the `block` is done executing.
62+
*/
63+
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
64+
val observer = Observer<T> { }
65+
try {
66+
observeForever(observer)
67+
block()
68+
} finally {
69+
removeObserver(observer)
70+
}
71+
}

app/src/test/java/com/example/android/architecture/blueprints/todoapp/TestUtil.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ fun assertLiveDataEventTriggered(
2323
liveData: LiveData<Event<String>>,
2424
taskId: String
2525
) {
26-
val value = liveData.awaitNextValue()
26+
val value = liveData.getOrAwaitValue()
2727
assertEquals(value.getContentIfNotHandled(), taskId)
2828
}
2929

3030
fun assertSnackbarMessage(snackbarLiveData: LiveData<Event<Int>>, messageId: Int) {
31-
val value: Event<Int> = snackbarLiveData.awaitNextValue()
31+
val value: Event<Int> = snackbarLiveData.getOrAwaitValue()
3232
assertEquals(value.getContentIfNotHandled(), messageId)
3333
}

app/src/test/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModelTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
1919
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
2020
import com.example.android.architecture.blueprints.todoapp.R.string
2121
import com.example.android.architecture.blueprints.todoapp.assertSnackbarMessage
22-
import com.example.android.architecture.blueprints.todoapp.awaitNextValue
2322
import com.example.android.architecture.blueprints.todoapp.data.Task
2423
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
24+
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
2525
import com.google.common.truth.Truth.assertThat
2626
import kotlinx.coroutines.ExperimentalCoroutinesApi
2727
import org.junit.Before
@@ -86,13 +86,13 @@ class AddEditTaskViewModelTest {
8686
addEditTaskViewModel.start(task.id)
8787

8888
// Then progress indicator is shown
89-
assertThat(addEditTaskViewModel.dataLoading.awaitNextValue()).isTrue()
89+
assertThat(addEditTaskViewModel.dataLoading.getOrAwaitValue()).isTrue()
9090

9191
// Execute pending coroutines actions
9292
mainCoroutineRule.resumeDispatcher()
9393

9494
// Then progress indicator is hidden
95-
assertThat(addEditTaskViewModel.dataLoading.awaitNextValue()).isFalse()
95+
assertThat(addEditTaskViewModel.dataLoading.getOrAwaitValue()).isFalse()
9696
}
9797

9898
@Test
@@ -104,9 +104,9 @@ class AddEditTaskViewModelTest {
104104
addEditTaskViewModel.start(task.id)
105105

106106
// Verify a task is loaded
107-
assertThat(addEditTaskViewModel.title.awaitNextValue()).isEqualTo(task.title)
108-
assertThat(addEditTaskViewModel.description.awaitNextValue()).isEqualTo(task.description)
109-
assertThat(addEditTaskViewModel.dataLoading.awaitNextValue()).isFalse()
107+
assertThat(addEditTaskViewModel.title.getOrAwaitValue()).isEqualTo(task.title)
108+
assertThat(addEditTaskViewModel.description.getOrAwaitValue()).isEqualTo(task.description)
109+
assertThat(addEditTaskViewModel.dataLoading.getOrAwaitValue()).isFalse()
110110
}
111111

112112
@Test

app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.example.android.architecture.blueprints.todoapp.data.source
1717

18+
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
1819
import com.example.android.architecture.blueprints.todoapp.data.Result
1920
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
2021
import com.example.android.architecture.blueprints.todoapp.data.Task
@@ -23,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
2324
import kotlinx.coroutines.ExperimentalCoroutinesApi
2425
import kotlinx.coroutines.test.runBlockingTest
2526
import org.junit.Before
27+
import org.junit.Rule
2628
import org.junit.Test
2729

2830
/**
@@ -44,14 +46,19 @@ class DefaultTasksRepositoryTest {
4446
// Class under test
4547
private lateinit var tasksRepository: DefaultTasksRepository
4648

49+
// Set the main coroutines dispatcher for unit testing.
50+
@ExperimentalCoroutinesApi
51+
@get:Rule
52+
var mainCoroutineRule = MainCoroutineRule()
53+
4754
@ExperimentalCoroutinesApi
4855
@Before
4956
fun createRepository() {
5057
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
5158
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
5259
// Get a reference to the class under test
5360
tasksRepository = DefaultTasksRepository(
54-
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
61+
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Main
5562
)
5663
}
5764

@@ -60,7 +67,7 @@ class DefaultTasksRepositoryTest {
6067
fun getTasks_emptyRepositoryAndUninitializedCache() = runBlockingTest {
6168
val emptySource = FakeDataSource()
6269
val tasksRepository = DefaultTasksRepository(
63-
emptySource, emptySource, Dispatchers.Unconfined
70+
emptySource, emptySource, Dispatchers.Main
6471
)
6572

6673
assertThat(tasksRepository.getTasks() is Success).isTrue()

app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModelTest.kt

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ package com.example.android.architecture.blueprints.todoapp.statistics
1818
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
1919
import com.example.android.architecture.blueprints.todoapp.FakeFailingTasksRemoteDataSource
2020
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
21-
import com.example.android.architecture.blueprints.todoapp.awaitNextValue
2221
import com.example.android.architecture.blueprints.todoapp.data.Task
2322
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
2423
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
24+
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
2525
import com.google.common.truth.Truth.assertThat
2626
import kotlinx.coroutines.Dispatchers
2727
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -61,7 +61,7 @@ class StatisticsViewModelTest {
6161
// Given an initialized StatisticsViewModel with no tasks
6262

6363
// Then the results are empty
64-
assertThat(statisticsViewModel.empty.awaitNextValue()).isTrue()
64+
assertThat(statisticsViewModel.empty.getOrAwaitValue()).isTrue()
6565
}
6666

6767
@Test
@@ -74,29 +74,28 @@ class StatisticsViewModelTest {
7474
tasksRepository.addTasks(task1, task2, task3, task4)
7575

7676
// Then the results are not empty
77-
assertThat(statisticsViewModel.empty.awaitNextValue())
77+
assertThat(statisticsViewModel.empty.getOrAwaitValue())
7878
.isFalse()
79-
assertThat(statisticsViewModel.activeTasksPercent.awaitNextValue())
79+
assertThat(statisticsViewModel.activeTasksPercent.getOrAwaitValue())
8080
.isEqualTo(25f)
81-
assertThat(statisticsViewModel.completedTasksPercent.awaitNextValue())
81+
assertThat(statisticsViewModel.completedTasksPercent.getOrAwaitValue())
8282
.isEqualTo(75f)
8383
}
8484

8585
@Test
86-
fun loadStatisticsWhenTasksAreUnavailable_CallErrorToDisplay() =
87-
mainCoroutineRule.runBlockingTest {
88-
val errorViewModel = StatisticsViewModel(
89-
DefaultTasksRepository(
90-
FakeFailingTasksRemoteDataSource,
91-
FakeFailingTasksRemoteDataSource,
92-
Dispatchers.Main // Main is set in MainCoroutineRule
93-
)
86+
fun loadStatisticsWhenTasksAreUnavailable_CallErrorToDisplay(){
87+
val errorViewModel = StatisticsViewModel(
88+
DefaultTasksRepository(
89+
FakeFailingTasksRemoteDataSource,
90+
FakeFailingTasksRemoteDataSource,
91+
Dispatchers.Main // Main is set in MainCoroutineRule
9492
)
93+
)
9594

96-
// Then an error message is shown
97-
assertThat(errorViewModel.empty.awaitNextValue()).isTrue()
98-
assertThat(errorViewModel.error.awaitNextValue()).isTrue()
99-
}
95+
// Then an error message is shown
96+
assertThat(errorViewModel.empty.getOrAwaitValue()).isTrue()
97+
assertThat(errorViewModel.error.getOrAwaitValue()).isTrue()
98+
}
10099

101100
@Test
102101
fun loadTasks_loading() {
@@ -107,12 +106,12 @@ class StatisticsViewModelTest {
107106
statisticsViewModel.refresh()
108107

109108
// Then progress indicator is shown
110-
assertThat(statisticsViewModel.dataLoading.awaitNextValue()).isTrue()
109+
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue()).isTrue()
111110

112111
// Execute pending coroutines actions
113112
mainCoroutineRule.resumeDispatcher()
114113

115114
// Then progress indicator is hidden
116-
assertThat(statisticsViewModel.dataLoading.awaitNextValue()).isFalse()
115+
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue()).isFalse()
117116
}
118117
}

app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
1919
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
2020
import com.example.android.architecture.blueprints.todoapp.R
2121
import com.example.android.architecture.blueprints.todoapp.assertSnackbarMessage
22-
import com.example.android.architecture.blueprints.todoapp.awaitNextValue
2322
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
2423
import com.example.android.architecture.blueprints.todoapp.data.Task
2524
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
25+
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
26+
import com.example.android.architecture.blueprints.todoapp.observeForTesting
2627
import com.google.common.truth.Truth.assertThat
2728
import kotlinx.coroutines.ExperimentalCoroutinesApi
2829
import kotlinx.coroutines.test.runBlockingTest
@@ -68,8 +69,8 @@ class TaskDetailViewModelTest {
6869
taskDetailViewModel.start(task.id)
6970

7071
// Then verify that the view was notified
71-
assertThat(taskDetailViewModel.task.awaitNextValue()?.title).isEqualTo(task.title)
72-
assertThat(taskDetailViewModel.task.awaitNextValue()?.description)
72+
assertThat(taskDetailViewModel.task.getOrAwaitValue()?.title).isEqualTo(task.title)
73+
assertThat(taskDetailViewModel.task.getOrAwaitValue()?.description)
7374
.isEqualTo(task.description)
7475
}
7576

@@ -78,7 +79,7 @@ class TaskDetailViewModelTest {
7879
// Load the ViewModel
7980
taskDetailViewModel.start(task.id)
8081
// Start observing to compute transformations
81-
taskDetailViewModel.task.awaitNextValue()
82+
taskDetailViewModel.task.getOrAwaitValue()
8283

8384
// Verify that the task was active initially
8485
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isFalse()
@@ -92,26 +93,27 @@ class TaskDetailViewModelTest {
9293
}
9394

9495
@Test
95-
fun activateTask() = runBlockingTest {
96+
fun activateTask() {
9697
task.isCompleted = true
9798

9899
// Load the ViewModel
99100
taskDetailViewModel.start(task.id)
100101
// Start observing to compute transformations
101-
taskDetailViewModel.task.awaitNextValue()
102+
taskDetailViewModel.task.observeForTesting {
102103

103-
taskDetailViewModel.task.observeForever { }
104+
// Verify that the task was completed initially
105+
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isTrue()
104106

105-
// Verify that the task was completed initially
106-
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isTrue()
107-
108-
// When the ViewModel is asked to complete the task
109-
taskDetailViewModel.setCompleted(false)
107+
// When the ViewModel is asked to complete the task
108+
taskDetailViewModel.setCompleted(false)
110109

111-
// Then the task is not completed and the snackbar shows the correct message
112-
val newTask = (tasksRepository.getTask(task.id) as Success).data
113-
assertTrue(newTask.isActive)
114-
assertSnackbarMessage(taskDetailViewModel.snackbarText, R.string.task_marked_active)
110+
runBlockingTest {
111+
// Then the task is not completed and the snackbar shows the correct message
112+
val newTask = (tasksRepository.getTask(task.id) as Success).data
113+
assertTrue(newTask.isActive)
114+
assertSnackbarMessage(taskDetailViewModel.snackbarText, R.string.task_marked_active)
115+
}
116+
}
115117
}
116118

117119
@Test
@@ -121,13 +123,11 @@ class TaskDetailViewModelTest {
121123

122124
// Given an initialized ViewModel with an active task
123125
taskDetailViewModel.start(task.id)
124-
// Start observing to compute transformations
125-
taskDetailViewModel.task.awaitNextValue()
126-
// Refresh to get
127-
128-
129-
// Then verify that data is not available
130-
assertThat(taskDetailViewModel.isDataAvailable.awaitNextValue()).isFalse()
126+
// Get the computed LiveData value
127+
taskDetailViewModel.task.observeForTesting {
128+
// Then verify that data is not available
129+
assertThat(taskDetailViewModel.isDataAvailable.getOrAwaitValue()).isFalse()
130+
}
131131
}
132132

133133
@Test
@@ -145,7 +145,7 @@ class TaskDetailViewModelTest {
145145
taskDetailViewModel.editTask()
146146

147147
// Then the event is triggered
148-
val value = taskDetailViewModel.editTaskEvent.awaitNextValue()
148+
val value = taskDetailViewModel.editTaskEvent.getOrAwaitValue()
149149
assertThat(value.getContentIfNotHandled()).isNotNull()
150150
}
151151

@@ -168,17 +168,18 @@ class TaskDetailViewModelTest {
168168
// Load the task in the viewmodel
169169
taskDetailViewModel.start(task.id)
170170
// Start observing to compute transformations
171-
taskDetailViewModel.task.observeForever { }
172-
// Force a refresh to show the loading indicator
173-
taskDetailViewModel.refresh()
171+
taskDetailViewModel.task.observeForTesting {
172+
// Force a refresh to show the loading indicator
173+
taskDetailViewModel.refresh()
174174

175-
// Then progress indicator is shown
176-
assertThat(taskDetailViewModel.dataLoading.awaitNextValue()).isTrue()
175+
// Then progress indicator is shown
176+
assertThat(taskDetailViewModel.dataLoading.getOrAwaitValue()).isTrue()
177177

178-
// Execute pending coroutines actions
179-
mainCoroutineRule.resumeDispatcher()
178+
// Execute pending coroutines actions
179+
mainCoroutineRule.resumeDispatcher()
180180

181-
// Then progress indicator is hidden
182-
assertThat(taskDetailViewModel.dataLoading.awaitNextValue()).isFalse()
181+
// Then progress indicator is hidden
182+
assertThat(taskDetailViewModel.dataLoading.getOrAwaitValue()).isFalse()
183+
}
183184
}
184185
}

0 commit comments

Comments
 (0)