diff --git a/coroutines-codelab/build.gradle b/coroutines-codelab/build.gradle index b3eb7644..2c79f372 100644 --- a/coroutines-codelab/build.gradle +++ b/coroutines-codelab/build.gradle @@ -27,7 +27,7 @@ buildscript { def arch_version = '2.1.0' def appcompat_version = '1.4.1' def constraint_layout_version = '2.1.3' - def coroutines_android_version = '1.5.2' + def coroutines_android_version = '1.6.0' def espresso_version = '3.4.0' def gson_version = '2.9.0' def junit_version = '4.13.2' diff --git a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt index 5a5cf63e..cd8b55ed 100644 --- a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt +++ b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt @@ -20,12 +20,14 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.example.android.kotlincoroutines.fakes.MainNetworkCompletableFake import com.example.android.kotlincoroutines.fakes.MainNetworkFake import com.example.android.kotlincoroutines.fakes.TitleDaoFake -import com.example.android.kotlincoroutines.main.utils.MainCoroutineScopeRule +import com.example.android.kotlincoroutines.main.utils.MainCoroutineRule import com.example.android.kotlincoroutines.main.utils.captureValues import com.example.android.kotlincoroutines.main.utils.getValueForTest -import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import okhttp3.MediaType import okhttp3.ResponseBody import org.junit.Before @@ -36,6 +38,7 @@ import org.junit.runners.JUnit4 import retrofit2.HttpException import retrofit2.Response +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(JUnit4::class) class MainViewModelTest { @@ -43,25 +46,25 @@ class MainViewModelTest { val instantTaskExecutorRule = InstantTaskExecutorRule() @get:Rule - val coroutineScope = MainCoroutineScopeRule() + val coroutineRule = MainCoroutineRule(UnconfinedTestDispatcher()) lateinit var subject: MainViewModel @Before fun setup() { subject = MainViewModel( - TitleRepository( - MainNetworkFake("OK"), - TitleDaoFake("initial") - )) + TitleRepository( + MainNetworkFake("OK"), + TitleDaoFake("initial") + )) } @Test - fun whenMainClicked_updatesTaps() { + fun whenMainClicked_updatesTaps() = runTest { subject.onMainViewClicked() - Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps") - coroutineScope.advanceTimeBy(1000) - Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps") + assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps") + advanceTimeBy(1001) + assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps") } @Test @@ -70,14 +73,14 @@ class MainViewModelTest { } @Test - fun whenSuccessfulTitleLoad_itShowsAndHidesSpinner() = coroutineScope.runBlockingTest { + fun whenSuccessfulTitleLoad_itShowsAndHidesSpinner() = runTest { val network = MainNetworkCompletableFake() subject = MainViewModel( - TitleRepository( - network, - TitleDaoFake("title") - ) + TitleRepository( + network, + TitleDaoFake("title") + ) ) subject.spinner.captureValues { @@ -89,13 +92,13 @@ class MainViewModelTest { } @Test - fun whenErrorTitleReload_itShowsErrorAndHidesSpinner() = coroutineScope.runBlockingTest { + fun whenErrorTitleReload_itShowsErrorAndHidesSpinner() = runTest { val network = MainNetworkCompletableFake() subject = MainViewModel( - TitleRepository( - network, - TitleDaoFake("title") - ) + TitleRepository( + network, + TitleDaoFake("title") + ) ) subject.spinner.captureValues { @@ -108,13 +111,13 @@ class MainViewModelTest { } @Test - fun whenErrorTitleReload_itShowsErrorText() = coroutineScope.runBlockingTest { + fun whenErrorTitleReload_itShowsErrorText() = runTest { val network = MainNetworkCompletableFake() subject = MainViewModel( - TitleRepository( - network, - TitleDaoFake("title") - ) + TitleRepository( + network, + TitleDaoFake("title") + ) ) subject.onMainViewClicked() @@ -125,13 +128,13 @@ class MainViewModelTest { } @Test - fun whenMainViewClicked_titleIsRefreshed() = coroutineScope.runBlockingTest { + fun whenMainViewClicked_titleIsRefreshed() = runTest { val titleDao = TitleDaoFake("title") subject = MainViewModel( - TitleRepository( - MainNetworkFake("OK"), - titleDao - ) + TitleRepository( + MainNetworkFake("OK"), + titleDao + ) ) subject.onMainViewClicked() assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK") @@ -139,10 +142,10 @@ class MainViewModelTest { private fun makeErrorResult(result: String): HttpException { return HttpException(Response.error( - 500, - ResponseBody.create( - MediaType.get("application/json"), - "\"$result\"") + 500, + ResponseBody.create( + MediaType.get("application/json"), + "\"$result\"") )) } } \ No newline at end of file diff --git a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/TitleRepositoryTest.kt b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/TitleRepositoryTest.kt index 152cce1e..bbc37a59 100644 --- a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/TitleRepositoryTest.kt +++ b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/TitleRepositoryTest.kt @@ -21,22 +21,25 @@ import com.example.android.kotlincoroutines.fakes.MainNetworkCompletableFake import com.example.android.kotlincoroutines.fakes.MainNetworkFake import com.example.android.kotlincoroutines.fakes.TitleDaoFake import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class TitleRepositoryTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Test - fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest { + fun whenRefreshTitleSuccess_insertsRows() = runTest { val titleDao = TitleDaoFake("title") val subject = TitleRepository( - MainNetworkFake("OK"), - titleDao + MainNetworkFake("OK"), + titleDao ) subject.refreshTitle() @@ -44,11 +47,11 @@ class TitleRepositoryTest { } @Test(expected = TitleRefreshError::class) - fun whenRefreshTitleTimeout_throws() = runBlockingTest { + fun whenRefreshTitleTimeout_throws() = runTest { val network = MainNetworkCompletableFake() val subject = TitleRepository( - network, - TitleDaoFake("title") + network, + TitleDaoFake("title") ) launch { diff --git a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineRule.kt b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineRule.kt new file mode 100644 index 00000000..15ad913e --- /dev/null +++ b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineRule.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.kotlincoroutines.main.utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * MainCoroutineRule installs a TestDispatcher for Dispatchers.Main. + * + * When using MainCoroutineRule, you should use runTest to contain your tests: + * + * ``` + * @Test + * fun usingRunTest() = runTest { + * aTestCoroutine() + * } + * ``` + * + * @param dispatcher if provided, this [TestDispatcher] will be used. + */ +@ExperimentalCoroutinesApi +class MainCoroutineRule(val dispatcher: TestDispatcher = StandardTestDispatcher()) : + TestWatcher() { + + override fun starting(description: Description?) { + super.starting(description) + // All injected dispatchers in a test should be TestDispatchers that share the same + // TestScheduler (available as dispatcher.scheduler). All TestDispatchers created after + // the Main dispatcher has been replaced will automatically share its scheduler. + Dispatchers.setMain(dispatcher) + + // If you need to create and inject test dispatchers before this happens, create a + // TestScheduler yourself, and pass it to all test dispatchers explicitly. + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + } +} diff --git a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineScopeRule.kt b/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineScopeRule.kt deleted file mode 100644 index cefc13d6..00000000 --- a/coroutines-codelab/finished_code/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineScopeRule.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.kotlincoroutines.main.utils - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -/** - * MainCoroutineRule installs a TestCoroutineDispatcher for Disptachers.Main. - * - * Since it extends TestCoroutineScope, you can directly launch coroutines on the MainCoroutineRule - * as a [CoroutineScope]: - * - * ``` - * mainCoroutineRule.launch { aTestCoroutine() } - * ``` - * - * All coroutines started on [MainCoroutineScopeRule] must complete (including timeouts) before the test - * finishes, or it will throw an exception. - * - * When using MainCoroutineRule you should always invoke runBlockingTest on it to avoid creating two - * instances of [TestCoroutineDispatcher] or [TestCoroutineScope] in your test: - * - * ``` - * @Test - * fun usingRunBlockingTest() = mainCoroutineRule.runBlockingTest { - * aTestCoroutine() - * } - * ``` - * - * You may call [DelayController] methods on [MainCoroutineScopeRule] and they will control the - * virtual-clock. - * - * ``` - * mainCoroutineRule.pauseDispatcher() - * // do some coroutines - * mainCoroutineRule.advanceUntilIdle() // run all pending coroutines until the dispatcher is idle - * ``` - * - * By default, [MainCoroutineScopeRule] will be in a *resumed* state. - * - * @param dispatcher if provided, this [TestCoroutineDispatcher] will be used. - */ -@ExperimentalCoroutinesApi -class MainCoroutineScopeRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : - TestWatcher(), - TestCoroutineScope by TestCoroutineScope(dispatcher) { - override fun starting(description: Description?) { - super.starting(description) - // If your codebase allows the injection of other dispatchers like - // Dispatchers.Default and Dispatchers.IO, consider injecting all of them here - // and renaming this class to `CoroutineScopeRule` - // - // All injected dispatchers in a test should point to a single instance of - // TestCoroutineDispatcher. - Dispatchers.setMain(dispatcher) - } - - override fun finished(description: Description?) { - super.finished(description) - cleanupTestCoroutines() - Dispatchers.resetMain() - } -} diff --git a/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt b/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt index c7983c04..434e4209 100644 --- a/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt +++ b/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/MainViewModelTest.kt @@ -19,14 +19,17 @@ package com.example.android.kotlincoroutines.main import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.example.android.kotlincoroutines.fakes.MainNetworkFake import com.example.android.kotlincoroutines.fakes.TitleDaoFake -import com.example.android.kotlincoroutines.main.utils.MainCoroutineScopeRule +import com.example.android.kotlincoroutines.main.utils.MainCoroutineRule +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Rule import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class MainViewModelTest { @get:Rule - val coroutineScope = MainCoroutineScopeRule() + val coroutineRule = MainCoroutineRule() + @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @@ -35,14 +38,14 @@ class MainViewModelTest { @Before fun setup() { subject = MainViewModel( - TitleRepository( - MainNetworkFake("OK"), - TitleDaoFake("initial") - )) + TitleRepository( + MainNetworkFake("OK"), + TitleDaoFake("initial") + )) } @Test fun whenMainClicked_updatesTaps() { // TODO: Write this } -} \ No newline at end of file +} diff --git a/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineRule.kt b/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineRule.kt new file mode 100644 index 00000000..15ad913e --- /dev/null +++ b/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineRule.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.kotlincoroutines.main.utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * MainCoroutineRule installs a TestDispatcher for Dispatchers.Main. + * + * When using MainCoroutineRule, you should use runTest to contain your tests: + * + * ``` + * @Test + * fun usingRunTest() = runTest { + * aTestCoroutine() + * } + * ``` + * + * @param dispatcher if provided, this [TestDispatcher] will be used. + */ +@ExperimentalCoroutinesApi +class MainCoroutineRule(val dispatcher: TestDispatcher = StandardTestDispatcher()) : + TestWatcher() { + + override fun starting(description: Description?) { + super.starting(description) + // All injected dispatchers in a test should be TestDispatchers that share the same + // TestScheduler (available as dispatcher.scheduler). All TestDispatchers created after + // the Main dispatcher has been replaced will automatically share its scheduler. + Dispatchers.setMain(dispatcher) + + // If you need to create and inject test dispatchers before this happens, create a + // TestScheduler yourself, and pass it to all test dispatchers explicitly. + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + } +} diff --git a/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineScopeRule.kt b/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineScopeRule.kt deleted file mode 100644 index cefc13d6..00000000 --- a/coroutines-codelab/start/src/test/java/com/example/android/kotlincoroutines/main/utils/MainCoroutineScopeRule.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.kotlincoroutines.main.utils - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -/** - * MainCoroutineRule installs a TestCoroutineDispatcher for Disptachers.Main. - * - * Since it extends TestCoroutineScope, you can directly launch coroutines on the MainCoroutineRule - * as a [CoroutineScope]: - * - * ``` - * mainCoroutineRule.launch { aTestCoroutine() } - * ``` - * - * All coroutines started on [MainCoroutineScopeRule] must complete (including timeouts) before the test - * finishes, or it will throw an exception. - * - * When using MainCoroutineRule you should always invoke runBlockingTest on it to avoid creating two - * instances of [TestCoroutineDispatcher] or [TestCoroutineScope] in your test: - * - * ``` - * @Test - * fun usingRunBlockingTest() = mainCoroutineRule.runBlockingTest { - * aTestCoroutine() - * } - * ``` - * - * You may call [DelayController] methods on [MainCoroutineScopeRule] and they will control the - * virtual-clock. - * - * ``` - * mainCoroutineRule.pauseDispatcher() - * // do some coroutines - * mainCoroutineRule.advanceUntilIdle() // run all pending coroutines until the dispatcher is idle - * ``` - * - * By default, [MainCoroutineScopeRule] will be in a *resumed* state. - * - * @param dispatcher if provided, this [TestCoroutineDispatcher] will be used. - */ -@ExperimentalCoroutinesApi -class MainCoroutineScopeRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : - TestWatcher(), - TestCoroutineScope by TestCoroutineScope(dispatcher) { - override fun starting(description: Description?) { - super.starting(description) - // If your codebase allows the injection of other dispatchers like - // Dispatchers.Default and Dispatchers.IO, consider injecting all of them here - // and renaming this class to `CoroutineScopeRule` - // - // All injected dispatchers in a test should point to a single instance of - // TestCoroutineDispatcher. - Dispatchers.setMain(dispatcher) - } - - override fun finished(description: Description?) { - super.finished(description) - cleanupTestCoroutines() - Dispatchers.resetMain() - } -}