Issue#67 Extract JUnit5 MainDispatcher
This commit is contained in:
parent
3b96a5d9eb
commit
7e019973e8
11 changed files with 112 additions and 57 deletions
|
|
@ -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>()
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue