From 7e019973e8d0efa16326671d1a94fb6830902265 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 11:47:22 +0300 Subject: [PATCH] Issue#67 Extract JUnit5 MainDispatcher --- .../org/fnives/test/showcase/di/DITest.kt | 4 +- .../showcase/testutils/TestMainDispatcher.kt | 39 ------------------- .../showcase/ui/auth/AuthViewModelTest.kt | 6 +-- .../showcase/ui/auth/CodeKataAuthViewModel.kt | 6 +-- .../showcase/ui/home/MainViewModelTest.kt | 6 +-- .../showcase/ui/splash/SplashViewModelTest.kt | 6 +-- codekata/viewmodel.instructionset.md | 8 ++-- test-util-junit5-android/build.gradle | 2 + .../testutil/StandardTestMainDispatcher.kt | 30 ++++++++++++++ .../android/testutil/TestMainDispatcher.kt | 32 +++++++++++++++ .../testutil/UnconfinedTestMainDispatcher.kt | 30 ++++++++++++++ 11 files changed, 112 insertions(+), 57 deletions(-) delete mode 100644 app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt create mode 100644 test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt create mode 100644 test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt create mode 100644 test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt diff --git a/app/src/test/java/org/fnives/test/showcase/di/DITest.kt b/app/src/test/java/org/fnives/test/showcase/di/DITest.kt index cf595ba..bc950e5 100644 --- a/app/src/test/java/org/fnives/test/showcase/di/DITest.kt +++ b/app/src/test/java/org/fnives/test/showcase/di/DITest.kt @@ -2,7 +2,7 @@ package org.fnives.test.showcase.di import android.content.Context import org.fnives.test.showcase.model.network.BaseUrl -import org.fnives.test.showcase.testutils.TestMainDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher import org.fnives.test.showcase.ui.auth.AuthViewModel import org.fnives.test.showcase.ui.home.MainViewModel import org.fnives.test.showcase.ui.splash.SplashViewModel @@ -20,7 +20,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -@ExtendWith(TestMainDispatcher::class) +@ExtendWith(StandardTestMainDispatcher::class) class DITest : KoinTest { private val authViewModel by inject() diff --git a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt b/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt deleted file mode 100644 index 77beff4..0000000 --- a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.fnives.test.showcase.testutils - -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.fnives.test.showcase.testutils.TestMainDispatcher.Companion.testDispatcher -import org.junit.jupiter.api.extension.AfterEachCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext - -/** - * Custom Junit5 Extension which replaces the main dispatcher with a [TestDispatcher] - * - * One can access the test dispatcher via [testDispatcher] static getter. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { - - override fun beforeEach(context: ExtensionContext?) { - val testDispatcher = StandardTestDispatcher() - privateTestDispatcher = testDispatcher - Dispatchers.setMain(testDispatcher) - } - - override fun afterEach(context: ExtensionContext?) { - Dispatchers.resetMain() - privateTestDispatcher = null - } - - companion object { - private var privateTestDispatcher: TestDispatcher? = null - val testDispatcher: TestDispatcher - get() = privateTestDispatcher - ?: throw IllegalStateException("TestMainDispatcher is in afterEach State") - } -} diff --git a/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt index 70767f5..0a1ee75 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt @@ -8,7 +8,7 @@ import org.fnives.test.showcase.core.login.LoginUseCase import org.fnives.test.showcase.model.auth.LoginCredentials import org.fnives.test.showcase.model.auth.LoginStatus import org.fnives.test.showcase.model.shared.Answer -import org.fnives.test.showcase.testutils.TestMainDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher import org.fnives.test.showcase.ui.shared.Event import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -27,13 +27,13 @@ import org.mockito.kotlin.whenever import java.util.stream.Stream @Suppress("TestFunctionName") -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) @OptIn(ExperimentalCoroutinesApi::class) internal class AuthViewModelTest { private lateinit var sut: AuthViewModel private lateinit var mockLoginUseCase: LoginUseCase - private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt b/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt index 4de4386..94c7055 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt @@ -3,20 +3,20 @@ package org.fnives.test.showcase.ui.auth import kotlinx.coroutines.ExperimentalCoroutinesApi import org.fnives.test.showcase.android.testutil.InstantExecutorExtension import org.fnives.test.showcase.core.login.LoginUseCase -import org.fnives.test.showcase.testutils.TestMainDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.kotlin.mock -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) @OptIn(ExperimentalCoroutinesApi::class) class CodeKataAuthViewModel { private lateinit var sut: AuthViewModel private lateinit var mockLoginUseCase: LoginUseCase - private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt index 6173944..8e7402f 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt @@ -15,7 +15,7 @@ import org.fnives.test.showcase.model.content.ContentId import org.fnives.test.showcase.model.content.FavouriteContent import org.fnives.test.showcase.model.content.ImageUrl import org.fnives.test.showcase.model.shared.Resource -import org.fnives.test.showcase.testutils.TestMainDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -29,7 +29,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @Suppress("TestFunctionName") -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) @OptIn(ExperimentalCoroutinesApi::class) internal class MainViewModelTest { @@ -39,7 +39,7 @@ internal class MainViewModelTest { private lateinit var mockFetchContentUseCase: FetchContentUseCase private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase - private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt index 01d59f1..4277a8d 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt @@ -4,7 +4,7 @@ import com.jraska.livedata.test import kotlinx.coroutines.ExperimentalCoroutinesApi import org.fnives.test.showcase.android.testutil.InstantExecutorExtension import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase -import org.fnives.test.showcase.testutils.TestMainDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher import org.fnives.test.showcase.ui.shared.Event import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -14,13 +14,13 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) @OptIn(ExperimentalCoroutinesApi::class) internal class SplashViewModelTest { private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var sut: SplashViewModel - private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/codekata/viewmodel.instructionset.md b/codekata/viewmodel.instructionset.md index e7c7f3e..b819eea 100644 --- a/codekata/viewmodel.instructionset.md +++ b/codekata/viewmodel.instructionset.md @@ -25,12 +25,12 @@ Our test class is `org.fnives.test.showcase.ui.splash.CodeKataSplashViewModelTes To properly test LiveData we need to make them instant, meaning as soon as the value is set the observers are updated. To Do this we can use a `InstantExecutorExtension`. -Also We need to set MainDispatcher as TestDispatcher, for this we can use the `TestMainDispatcher` Extension. +Also We need to set MainDispatcher as TestDispatcher, for this we can use the `StandardTestMainDispatcher` Extension. To add this to our TestClass we need to do the following: ```kotlin -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) class CodeKataSplashViewModelTest { ``` @@ -41,7 +41,7 @@ Next let's set up our System Under Test as usual: ```kotlin private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var sut: SplashViewModel -private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler // just a shortcut +private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler // just a shortcut @BeforeEach fun setUp() { @@ -69,7 +69,7 @@ val navigateToTestObserver = sut.navigateTo.test() Since the action takes place in the ViewModel constructor, instead of additional calls, we need to simulate that time has elapsed. -Note: the `TestMainDispatcher` Extension we are using sets `StandardTestDispatcher` as the dispatcher for `Dispatcher.Main`, that's why our test is linear and not shaky. +Note: the `StandardTestMainDispatcher` Extension we are using sets `StandardTestDispatcher` as the dispatcher for `Dispatcher.Main`, that's why our test is linear and not shaky. ```kotlin testScheduler.advanceTimeBy(501) diff --git a/test-util-junit5-android/build.gradle b/test-util-junit5-android/build.gradle index dd13d96..23b7bdc 100644 --- a/test-util-junit5-android/build.gradle +++ b/test-util-junit5-android/build.gradle @@ -32,4 +32,6 @@ android { dependencies { implementation "org.junit.jupiter:junit-jupiter-engine:$junit5_version" implementation "androidx.arch.core:core-runtime:$arch_core_version" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" } \ No newline at end of file diff --git a/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt new file mode 100644 index 0000000..9fda7c8 --- /dev/null +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt @@ -0,0 +1,30 @@ +package org.fnives.test.showcase.android.testutil + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher.Companion.testDispatcher + +/** + * Custom Junit5 Extension which replaces the main dispatcher with a Standard [TestDispatcher] + * + * One can access the test dispatcher via [testDispatcher] static getter. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class StandardTestMainDispatcher : TestMainDispatcher() { + + override var createdTestDispatcher: TestDispatcher? + get() = privateTestDispatcher + set(value) { + privateTestDispatcher = value + } + + override fun createDispatcher(): TestDispatcher = StandardTestDispatcher() + + companion object { + private var privateTestDispatcher: TestDispatcher? = null + val testDispatcher: TestDispatcher + get() = privateTestDispatcher + ?: throw IllegalStateException("StandardTestMainDispatcher is in afterEach State") + } +} diff --git a/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt new file mode 100644 index 0000000..c7db54e --- /dev/null +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt @@ -0,0 +1,32 @@ +package org.fnives.test.showcase.android.testutil + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext + +/** + * Custom Junit5 Extension which replaces the main dispatcher with a [TestDispatcher] + */ +@OptIn(ExperimentalCoroutinesApi::class) +abstract class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { + + protected abstract var createdTestDispatcher: TestDispatcher? + + abstract fun createDispatcher() : TestDispatcher + + final override fun beforeEach(context: ExtensionContext?) { + val testDispatcher = createDispatcher() + createdTestDispatcher = testDispatcher + Dispatchers.setMain(testDispatcher) + } + + final override fun afterEach(context: ExtensionContext?) { + Dispatchers.resetMain() + createdTestDispatcher = null + } +} \ No newline at end of file diff --git a/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt new file mode 100644 index 0000000..5113237 --- /dev/null +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt @@ -0,0 +1,30 @@ +package org.fnives.test.showcase.android.testutil + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.fnives.test.showcase.android.testutil.UnconfinedTestMainDispatcher.Companion.testDispatcher + +/** + * Custom Junit5 Extension which replaces the main dispatcher with a Unconfined [TestDispatcher] + * + * One can access the test dispatcher via [testDispatcher] static getter. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class UnconfinedTestMainDispatcher : TestMainDispatcher() { + + override var createdTestDispatcher: TestDispatcher? + get() = privateTestDispatcher + set(value) { + privateTestDispatcher = value + } + + override fun createDispatcher(): TestDispatcher = UnconfinedTestDispatcher() + + companion object { + private var privateTestDispatcher: TestDispatcher? = null + val testDispatcher: TestDispatcher + get() = privateTestDispatcher + ?: throw IllegalStateException("StandardTestMainDispatcher is in afterEach State") + } +}