Skip to content

Commit 94e7656

Browse files
Merge pull request #685 from android/reactive_testfix
Fixes tests in reactive branch
2 parents f4128dd + 4587b1a commit 94e7656

File tree

12 files changed

+167
-98
lines changed

12 files changed

+167
-98
lines changed

app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt renamed to app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ import org.junit.runner.RunWith
5151
/**
5252
* Tests for the [DrawerLayout] layout component in [TasksActivity] which manages
5353
* navigation within the app.
54+
*
55+
* UI tests usually use [ActivityTestRule] but there's no API to perform an action before
56+
* each test. The workaround is to use `ActivityScenario.launch()` and `ActivityScenario.close()`.
5457
*/
5558
@RunWith(AndroidJUnit4::class)
5659
@LargeTest
@@ -117,6 +120,8 @@ class AppNavigationTest {
117120

118121
// Check that tasks screen was opened.
119122
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
123+
// When using ActivityScenario.launch, always call close()
124+
activityScenario.close()
120125
}
121126

122127
@Test
@@ -140,6 +145,8 @@ class AppNavigationTest {
140145
// Check if drawer is open
141146
onView(withId(R.id.drawer_layout))
142147
.check(matches(isOpen(Gravity.START))) // Left drawer is open open.
148+
// When using ActivityScenario.launch, always call close()
149+
activityScenario.close()
143150
}
144151

145152
@Test
@@ -168,6 +175,8 @@ class AppNavigationTest {
168175
// Then check that the drawer is open
169176
onView(withId(R.id.drawer_layout))
170177
.check(matches(isOpen(Gravity.START))) // Left drawer is open open.
178+
// When using ActivityScenario.launch, always call close()
179+
activityScenario.close()
171180
}
172181

173182
@Test
@@ -201,6 +210,8 @@ class AppNavigationTest {
201210
)
202211
).perform(click())
203212
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
213+
// When using ActivityScenario.launch, always call close()
214+
activityScenario.close()
204215
}
205216

206217
@Test
@@ -224,5 +235,7 @@ class AppNavigationTest {
224235
// Confirm that if we click back a second time, we end up back at the home screen
225236
pressBack()
226237
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
238+
// When using ActivityScenario.launch, always call close()
239+
activityScenario.close()
227240
}
228241
}

app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksActivityTest.kt renamed to app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksActivityTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ import org.junit.runner.RunWith
5252

5353
/**
5454
* Large End-to-End test for the tasks module.
55+
*
56+
* UI tests usually use [ActivityTestRule] but there's no API to perform an action before
57+
* each test. The workaround is to use `ActivityScenario.launch()` and `ActivityScenario.close()`.
5558
*/
5659
@RunWith(AndroidJUnit4::class)
5760
@LargeTest
@@ -116,6 +119,8 @@ class TasksActivityTest {
116119
onView(withText("NEW TITLE")).check(matches(isDisplayed()))
117120
// Verify previous task is not displayed
118121
onView(withText("TITLE1")).check(doesNotExist())
122+
// Make sure the activity is closed before resetting the db:
123+
activityScenario.close()
119124
}
120125

121126
@Test
@@ -141,6 +146,8 @@ class TasksActivityTest {
141146
onView(withId(R.id.menu_filter)).perform(click())
142147
onView(withText(string.nav_all)).perform(click())
143148
onView(withText("TITLE1")).check(doesNotExist())
149+
// Make sure the activity is closed before resetting the db:
150+
activityScenario.close()
144151
}
145152

146153
@Test
@@ -162,6 +169,8 @@ class TasksActivityTest {
162169
onView(withText(string.nav_all)).perform(click())
163170
onView(withText("TITLE1")).check(matches(isDisplayed()))
164171
onView(withText("TITLE2")).check(doesNotExist())
172+
// Make sure the activity is closed before resetting the db:
173+
activityScenario.close()
165174
}
166175

167176
@Test
@@ -190,6 +199,8 @@ class TasksActivityTest {
190199
// Check that the task is marked as completed
191200
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
192201
.check(matches(isChecked()))
202+
// Make sure the activity is closed before resetting the db:
203+
activityScenario.close()
193204
}
194205

195206
@Test
@@ -217,6 +228,8 @@ class TasksActivityTest {
217228
// Check that the task is marked as active
218229
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
219230
.check(matches(not(isChecked())))
231+
// Make sure the activity is closed before resetting the db:
232+
activityScenario.close()
220233
}
221234

222235
@Test
@@ -275,6 +288,8 @@ class TasksActivityTest {
275288
// Check that the task is marked as active
276289
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
277290
.check(matches(isChecked()))
291+
// Make sure the activity is closed before resetting the db:
292+
activityScenario.close()
278293
}
279294

280295
@Test
@@ -292,5 +307,7 @@ class TasksActivityTest {
292307

293308
// Then verify task is displayed on screen
294309
onView(withText("title")).check(matches(isDisplayed()))
310+
// Make sure the activity is closed before resetting the db:
311+
activityScenario.close()
295312
}
296313
}

app/src/mock/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ object ServiceLocator {
4545
}
4646

4747
private fun createTasksRepository(context: Context): TasksRepository {
48-
return DefaultTasksRepository(FakeTasksRemoteDataSource, createTaskLocalDataSource(context))
48+
val newRepo = DefaultTasksRepository(FakeTasksRemoteDataSource, createTaskLocalDataSource(context))
49+
tasksRepository = newRepo
50+
return newRepo
4951
}
5052

5153
private fun createTaskLocalDataSource(context: Context): TasksDataSource {

app/src/prod/java/com/example/android/architecture/blueprints/todoapp/ServiceLocator.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ object ServiceLocator {
4646
}
4747

4848
private fun createTasksRepository(context: Context): TasksRepository {
49-
return DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
49+
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
50+
tasksRepository = newRepo
51+
return newRepo
5052
}
5153

5254
private fun createTaskLocalDataSource(context: Context): TasksDataSource {

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
}

0 commit comments

Comments
 (0)