Skip to content
This repository was archived by the owner on Dec 11, 2024. It is now read-only.

Commit 043c38c

Browse files
Merge pull request #132 from googlecodelabs/coroutinesTest
Migrates to coroutines-test
2 parents 720da61 + 16ccb69 commit 043c38c

File tree

16 files changed

+205
-211
lines changed

16 files changed

+205
-211
lines changed

Diff for: .circleci/config.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ config_android:
44
- image: circleci/android:api-28
55
working_directory: ~/project
66
environment:
7-
JAVA_TOOL_OPTIONS: -Xmx1024m
8-
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Dkotlin.incremental=false
7+
JAVA_TOOL_OPTIONS: "-Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
8+
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Djava.util.concurrent.ForkJoinPool.common.parallelism=2 -Dkotlin.incremental=false"
99
TERM: dumb
1010
setup_ftl:
1111
- run:
@@ -20,8 +20,8 @@ jobs:
2020
- image: circleci/android:api-28
2121
working_directory: ~/project
2222
environment:
23-
- JAVA_TOOL_OPTIONS: -Xmx1024m
24-
- GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Dkotlin.incremental=false
23+
- JAVA_TOOL_OPTIONS: "-Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
24+
- GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Djava.util.concurrent.ForkJoinPool.common.parallelism=2 -Dkotlin.incremental=false"
2525
- TERM: dumb
2626
steps:
2727
- checkout

Diff for: app/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ dependencies {
134134
androidTestImplementation "junit:junit:$junitVersion"
135135
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
136136
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
137+
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
137138

138139
// AndroidX Test - JVM testing
139140
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

Diff for: app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt

+18-12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.example.android.architecture.blueprints.todoapp.data.Result
2727
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
2828
import com.example.android.architecture.blueprints.todoapp.data.Task
2929
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
30+
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource
3031
import kotlinx.coroutines.launch
3132

3233
/**
@@ -85,34 +86,39 @@ class TaskDetailViewModel(
8586
}
8687
}
8788

88-
89-
fun start(taskId: String?) = viewModelScope.launch {
90-
if (taskId != null) {
91-
_dataLoading.value = true
92-
tasksRepository.getTask(taskId, false).let { result ->
93-
if (result is Success) {
94-
onTaskLoaded(result.data)
95-
} else {
96-
onDataNotAvailable(result)
89+
fun start(taskId: String?) {
90+
_dataLoading.value = true
91+
92+
// Espresso does not work well with coroutines yet. See
93+
// https://github.com/Kotlin/kotlinx.coroutines/issues/982
94+
EspressoIdlingResource.increment() // Set app as busy.
95+
96+
viewModelScope.launch {
97+
if (taskId != null) {
98+
tasksRepository.getTask(taskId, false).let { result ->
99+
if (result is Success) {
100+
onTaskLoaded(result.data)
101+
} else {
102+
onDataNotAvailable(result)
103+
}
97104
}
98105
}
106+
_dataLoading.value = false
107+
EspressoIdlingResource.decrement() // Set app as idle.
99108
}
100109
}
101110

102-
103111
private fun setTask(task: Task?) {
104112
this._task.value = task
105113
_isDataAvailable.value = task != null
106114
}
107115

108116
private fun onTaskLoaded(task: Task) {
109117
setTask(task)
110-
_dataLoading.value = false
111118
}
112119

113120
private fun onDataNotAvailable(result: Result<Task>) {
114121
_task.value = null
115-
_dataLoading.value = false
116122
_isDataAvailable.value = false
117123
}
118124

Diff for: app/src/test/java/com/example/android/architecture/blueprints/todoapp/CoroutinesTestRule.kt renamed to app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/MainCoroutineRule.kt

+24-22
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,46 @@ package com.example.android.architecture.blueprints.todoapp
1919
import kotlinx.coroutines.CoroutineDispatcher
2020
import kotlinx.coroutines.Dispatchers
2121
import kotlinx.coroutines.ExperimentalCoroutinesApi
22-
import kotlinx.coroutines.ObsoleteCoroutinesApi
23-
import kotlinx.coroutines.asCoroutineDispatcher
24-
import kotlinx.coroutines.test.TestCoroutineContext
22+
import kotlinx.coroutines.test.TestCoroutineScope
2523
import kotlinx.coroutines.test.resetMain
2624
import kotlinx.coroutines.test.setMain
2725
import org.junit.rules.TestWatcher
2826
import org.junit.runner.Description
29-
import java.util.concurrent.Executors
3027
import kotlin.coroutines.ContinuationInterceptor
3128

3229
/**
33-
* Sets the main coroutines dispatcher for unit testing.
30+
* Sets the main coroutines dispatcher to a [TestCoroutineScope] for unit testing. A
31+
* [TestCoroutineScope] provides control over the execution of coroutines.
3432
*
35-
* Uses the deprecated TestCoroutineContext if provided. Otherwise it uses a new single thread
36-
* executor.
37-
* See https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471
38-
* and https://github.com/Kotlin/kotlinx.coroutines/issues/541
33+
* Declare it as a JUnit Rule:
34+
*
35+
* ```
36+
* @get:Rule
37+
* var mainCoroutineRule = MainCoroutineRule()
38+
* ```
39+
*
40+
* Use it directly as a [TestCoroutineScope]:
41+
*
42+
* ```
43+
* mainCoroutineRule.pauseDispatcher()
44+
* ...
45+
* mainCoroutineRule.resumeDispatcher()
46+
* ...
47+
* mainCoroutineRule.runBlockingTest { }
48+
* ...
49+
*
50+
* ```
3951
*/
40-
@ObsoleteCoroutinesApi
4152
@ExperimentalCoroutinesApi
42-
class ViewModelScopeMainDispatcherRule(
43-
private val testContext: TestCoroutineContext? = null
44-
) : TestWatcher() {
45-
46-
private val singleThreadExecutor = Executors.newSingleThreadExecutor()
53+
class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
4754

4855
override fun starting(description: Description?) {
4956
super.starting(description)
50-
if (testContext != null) {
51-
Dispatchers.setMain(testContext[ContinuationInterceptor] as CoroutineDispatcher)
52-
} else {
53-
Dispatchers.setMain(singleThreadExecutor.asCoroutineDispatcher())
54-
}
57+
Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
5558
}
5659

5760
override fun finished(description: Description?) {
5861
super.finished(description)
59-
singleThreadExecutor.shutdownNow()
6062
Dispatchers.resetMain()
6163
}
62-
}
64+
}

Diff for: app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragmentTest.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepos
3636
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
3737
import com.example.android.architecture.blueprints.todoapp.util.ADD_EDIT_RESULT_OK
3838
import com.example.android.architecture.blueprints.todoapp.util.getTasksBlocking
39-
import kotlinx.coroutines.ObsoleteCoroutinesApi
40-
import kotlinx.coroutines.runBlocking
39+
import kotlinx.coroutines.ExperimentalCoroutinesApi
40+
import kotlinx.coroutines.test.runBlockingTest
4141
import org.junit.After
4242
import org.junit.Assert.assertEquals
4343
import org.junit.Before
@@ -51,11 +51,11 @@ import org.robolectric.annotation.TextLayoutMode
5151
/**
5252
* Integration test for the Add Task screen.
5353
*/
54-
@ObsoleteCoroutinesApi
5554
@RunWith(AndroidJUnit4::class)
5655
@MediumTest
5756
@LooperMode(LooperMode.Mode.PAUSED)
5857
@TextLayoutMode(TextLayoutMode.Mode.REALISTIC)
58+
@ExperimentalCoroutinesApi
5959
class AddEditTaskFragmentTest {
6060
private lateinit var repository: TasksRepository
6161

@@ -66,7 +66,7 @@ class AddEditTaskFragmentTest {
6666
}
6767

6868
@After
69-
fun cleanupDb() = runBlocking {
69+
fun cleanupDb() = runBlockingTest {
7070
ServiceLocator.resetRepository()
7171
}
7272

Diff for: app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDaoTest.kt

+23-9
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,40 @@
1616

1717
package com.example.android.architecture.blueprints.todoapp.data.source.local
1818

19+
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
1920
import androidx.room.Room
2021
import androidx.test.core.app.ApplicationProvider.getApplicationContext
2122
import androidx.test.ext.junit.runners.AndroidJUnit4
2223
import androidx.test.filters.SmallTest
24+
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
2325
import com.example.android.architecture.blueprints.todoapp.data.Task
24-
import kotlinx.coroutines.runBlocking
26+
import kotlinx.coroutines.ExperimentalCoroutinesApi
27+
import kotlinx.coroutines.test.runBlockingTest
2528
import org.hamcrest.CoreMatchers.`is`
2629
import org.hamcrest.CoreMatchers.notNullValue
2730
import org.hamcrest.MatcherAssert.assertThat
2831
import org.junit.After
2932
import org.junit.Before
33+
import org.junit.Rule
3034
import org.junit.Test
3135
import org.junit.runner.RunWith
3236

37+
@ExperimentalCoroutinesApi
3338
@RunWith(AndroidJUnit4::class)
3439
@SmallTest
3540
class TasksDaoTest {
3641

3742
private lateinit var database: ToDoDatabase
3843

44+
// Set the main coroutines dispatcher for unit testing.
45+
@ExperimentalCoroutinesApi
46+
@get:Rule
47+
var mainCoroutineRule = MainCoroutineRule()
48+
49+
// Executes each task synchronously using Architecture Components.
50+
@get:Rule
51+
var instantExecutorRule = InstantTaskExecutorRule()
52+
3953
@Before
4054
fun initDb() {
4155
// using an in-memory database because the information stored here disappears when the
@@ -50,7 +64,7 @@ class TasksDaoTest {
5064
fun closeDb() = database.close()
5165

5266
@Test
53-
fun insertTaskAndGetById() = runBlocking {
67+
fun insertTaskAndGetById() = runBlockingTest {
5468
// GIVEN - insert a task
5569
val task = Task("title", "description")
5670
database.taskDao().insertTask(task)
@@ -67,7 +81,7 @@ class TasksDaoTest {
6781
}
6882

6983
@Test
70-
fun insertTaskReplacesOnConflict() = runBlocking {
84+
fun insertTaskReplacesOnConflict() = runBlockingTest {
7185
// Given that a task is inserted
7286
val task = Task("title", "description")
7387
database.taskDao().insertTask(task)
@@ -85,7 +99,7 @@ class TasksDaoTest {
8599
}
86100

87101
@Test
88-
fun insertTaskAndGetTasks() = runBlocking {
102+
fun insertTaskAndGetTasks() = runBlockingTest {
89103
// GIVEN - insert a task
90104
val task = Task("title", "description")
91105
database.taskDao().insertTask(task)
@@ -102,7 +116,7 @@ class TasksDaoTest {
102116
}
103117

104118
@Test
105-
fun updateTaskAndGetById() = runBlocking {
119+
fun updateTaskAndGetById() = runBlockingTest {
106120
// When inserting a task
107121
val originalTask = Task("title", "description")
108122
database.taskDao().insertTask(originalTask)
@@ -120,7 +134,7 @@ class TasksDaoTest {
120134
}
121135

122136
@Test
123-
fun updateCompletedAndGetById() = runBlocking {
137+
fun updateCompletedAndGetById() = runBlockingTest {
124138
// When inserting a task
125139
val task = Task("title", "description", true)
126140
database.taskDao().insertTask(task)
@@ -137,7 +151,7 @@ class TasksDaoTest {
137151
}
138152

139153
@Test
140-
fun deleteTaskByIdAndGettingTasks() = runBlocking {
154+
fun deleteTaskByIdAndGettingTasks() = runBlockingTest {
141155
// Given a task inserted
142156
val task = Task("title", "description")
143157
database.taskDao().insertTask(task)
@@ -151,7 +165,7 @@ class TasksDaoTest {
151165
}
152166

153167
@Test
154-
fun deleteTasksAndGettingTasks() = runBlocking {
168+
fun deleteTasksAndGettingTasks() = runBlockingTest {
155169
// Given a task inserted
156170
database.taskDao().insertTask(Task("title", "description"))
157171

@@ -164,7 +178,7 @@ class TasksDaoTest {
164178
}
165179

166180
@Test
167-
fun deleteCompletedTasksAndGettingTasks() = runBlocking {
181+
fun deleteCompletedTasksAndGettingTasks() = runBlockingTest {
168182
// Given a completed task inserted
169183
database.taskDao().insertTask(Task("completed", "task", true))
170184

0 commit comments

Comments
 (0)