Issue#67 Extract JUnit5 MainDispatcher

This commit is contained in:
Gergely Hegedus 2022-07-12 11:47:22 +03:00
parent 3b96a5d9eb
commit 7e019973e8
11 changed files with 112 additions and 57 deletions

View file

@ -2,7 +2,7 @@ package org.fnives.test.showcase.di
import android.content.Context import android.content.Context
import org.fnives.test.showcase.model.network.BaseUrl 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.auth.AuthViewModel
import org.fnives.test.showcase.ui.home.MainViewModel import org.fnives.test.showcase.ui.home.MainViewModel
import org.fnives.test.showcase.ui.splash.SplashViewModel 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.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
@ExtendWith(TestMainDispatcher::class) @ExtendWith(StandardTestMainDispatcher::class)
class DITest : KoinTest { class DITest : KoinTest {
private val authViewModel by inject<AuthViewModel>() private val authViewModel by inject<AuthViewModel>()

View file

@ -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")
}
}

View file

@ -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.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer 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.fnives.test.showcase.ui.shared.Event
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@ -27,13 +27,13 @@ import org.mockito.kotlin.whenever
import java.util.stream.Stream import java.util.stream.Stream
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class AuthViewModelTest { internal class AuthViewModelTest {
private lateinit var sut: AuthViewModel private lateinit var sut: AuthViewModel
private lateinit var mockLoginUseCase: LoginUseCase private lateinit var mockLoginUseCase: LoginUseCase
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -3,20 +3,20 @@ package org.fnives.test.showcase.ui.auth
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.fnives.test.showcase.android.testutil.InstantExecutorExtension import org.fnives.test.showcase.android.testutil.InstantExecutorExtension
import org.fnives.test.showcase.core.login.LoginUseCase 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.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class CodeKataAuthViewModel { class CodeKataAuthViewModel {
private lateinit var sut: AuthViewModel private lateinit var sut: AuthViewModel
private lateinit var mockLoginUseCase: LoginUseCase private lateinit var mockLoginUseCase: LoginUseCase
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -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.FavouriteContent
import org.fnives.test.showcase.model.content.ImageUrl import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource 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.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -29,7 +29,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class MainViewModelTest { internal class MainViewModelTest {
@ -39,7 +39,7 @@ internal class MainViewModelTest {
private lateinit var mockFetchContentUseCase: FetchContentUseCase private lateinit var mockFetchContentUseCase: FetchContentUseCase
private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase
private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -4,7 +4,7 @@ import com.jraska.livedata.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.fnives.test.showcase.android.testutil.InstantExecutorExtension import org.fnives.test.showcase.android.testutil.InstantExecutorExtension
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase 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.fnives.test.showcase.ui.shared.Event
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@ -14,13 +14,13 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class SplashViewModelTest { internal class SplashViewModelTest {
private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase
private lateinit var sut: SplashViewModel private lateinit var sut: SplashViewModel
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -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`. 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: To add this to our TestClass we need to do the following:
```kotlin ```kotlin
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
class CodeKataSplashViewModelTest { class CodeKataSplashViewModelTest {
``` ```
@ -41,7 +41,7 @@ Next let's set up our System Under Test as usual:
```kotlin ```kotlin
private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase
private lateinit var sut: SplashViewModel 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 @BeforeEach
fun setUp() { 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. 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 ```kotlin
testScheduler.advanceTimeBy(501) testScheduler.advanceTimeBy(501)

View file

@ -32,4 +32,6 @@ android {
dependencies { dependencies {
implementation "org.junit.jupiter:junit-jupiter-engine:$junit5_version" implementation "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
implementation "androidx.arch.core:core-runtime:$arch_core_version" implementation "androidx.arch.core:core-runtime:$arch_core_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
} }

View file

@ -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")
}
}

View file

@ -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
}
}

View file

@ -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")
}
}