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 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<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.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() {

View file

@ -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() {

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.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() {

View file

@ -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() {

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`.
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)

View file

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

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