Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Bump Turbine testing library and refactor `MviTurbineExtension`
The 1.1.0 version of [Turbine](https://github.com/cashapp/turbine/releases/tag/1.1.0) was released and introduced a new `turbineScope` that needs to be used when using `testIn`. This impacts our `MviTurbineExtension`, which needs to be rewritten to be compatible.

The benefit is that unconsumed events will fail the test automatically.


Discussion hints:
nan

Pull request: Update Turbine 0.13.0 -> 1.1.0
Update Turbine 0.13.0 -> 1.1.0 and add `runMviTest` to combine `runTest` with `turbineScope`.

Supporting change for #8163

Resolves #7310

Test command: ./gradlew :feature:account:server:settings:testDebugUnitTest --tests "app.k9mail.feature.account.server.settings.ui.outgoing.OutgoingServerSettingsViewModelTest" --tests "app.k9mail.feature.account.server.settings.ui.incoming.IncomingServerSettingsViewModelTest" :feature:account:setup:testDebugUnitTest --tests "app.k9mail.feature.account.setup.ui.createaccount.CreateAccountViewModelTest" --tests "app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsViewModelTest" --tests "app.k9mail.feature.account.setup.ui.specialfolders.SpecialFoldersViewModelTest" --tests "app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModelTest" --tests "app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsViewModelTest" :feature:account:oauth:testDebugUnitTest --tests "app.k9mail.feature.account.oauth.ui.AccountOAuthViewModelTest" :feature:account:edit:testDebugUnitTest --tests "app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyIncomingServerSettingsViewModelTest" --tests "app.k9mail.feature.account.edit.ui.server.settings.modify.ModifyOutgoingServerSettingsViewModelTest" :feature:navigation:drawer:testDebugUnitTest --tests "app.k9mail.feature.navigation.drawer.ui.DrawerViewModelTest" :feature:migration:qrcode:testDebugUnitTest --tests "app.k9mail.feature.migration.qrcode.ui.QrCodeScannerViewModelTest"

Fix patch:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5ede9e867ec..8e45cff5c4e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -96,7 +96,7 @@ searchPreference = "v2.3.0"
 spotlessPlugin = "6.25.0"
 timber = "5.0.1"
 tokenautocomplete = "4.0.0-beta01-k9mail02"
-turbine = "0.13.0"
+turbine = "1.1.0"
 xmlpull = "1.0"
 zxing = "3.5.3"
 


Test patch:
diff --git a/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/EventStateTestUtil.kt b/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/EventStateTestUtil.kt
index 2895ed3acdb..d73ff168461 100644
--- a/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/EventStateTestUtil.kt
+++ b/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/EventStateTestUtil.kt
@@ -1,35 +1,21 @@
 package app.k9mail.core.ui.compose.testing.mvi
 
-import app.cash.turbine.testIn
 import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
-import assertk.assertions.assertThatAndTurbinesConsumed
+import assertk.assertThat
 import assertk.assertions.isEqualTo
-import kotlinx.coroutines.CoroutineScope
 
-suspend fun <STATE, EVENT, EFFECT> eventStateTest(
+/**
+ * Tests that the state of the [viewModel] changes as expected when the [event] is sent.
+ */
+suspend inline fun <reified STATE, EVENT, EFFECT> MviContext.eventStateTest(
     viewModel: UnidirectionalViewModel<STATE, EVENT, EFFECT>,
     initialState: STATE,
     event: EVENT,
     expectedState: STATE,
-    coroutineScope: CoroutineScope,
 ) {
-    val stateTurbine = viewModel.state.testIn(coroutineScope)
-    val effectTurbine = viewModel.effect.testIn(coroutineScope)
-    val turbines = listOf(stateTurbine, effectTurbine)
-
-    assertThatAndTurbinesConsumed(
-        actual = stateTurbine.awaitItem(),
-        turbines = turbines,
-    ) {
-        isEqualTo(initialState)
-    }
+    val turbines = turbinesWithInitialStateCheck(viewModel, initialState)
 
     viewModel.event(event)
 
-    assertThatAndTurbinesConsumed(
-        actual = stateTurbine.awaitItem(),
-        turbines = turbines,
-    ) {
-        isEqualTo(expectedState)
-    }
+    assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(expectedState)
 }
diff --git a/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt b/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt
index 28b56e3720e..2a072c52502 100644
--- a/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt
+++ b/core/ui/compose/testing/src/main/kotlin/app/k9mail/core/ui/compose/testing/mvi/MviTurbineExtension.kt
@@ -1,31 +1,73 @@
 package app.k9mail.core.ui.compose.testing.mvi
 
 import app.cash.turbine.ReceiveTurbine
-import app.cash.turbine.testIn
+import app.cash.turbine.TurbineContext
+import app.cash.turbine.turbineScope
 import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
 import assertk.Assert
 import assertk.all
 import assertk.assertThat
 import assertk.assertions.isEqualTo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+
+/**
+ * The `runMviTest` function is a wrapper around `runTest` and `turbineScope`
+ * that provides a MviContext to the test body.
+ */
+fun runMviTest(
+    testBody: suspend MviContext.() -> Unit,
+) {
+    runTest {
+        val testScope = this
+        turbineScope {
+            val turbineContext = this
+            testBody(
+                DefaultMviContext(
+                    testScope = testScope,
+                    turbineContext = turbineContext,
+                ),
+            )
+        }
+    }
+}
+
+interface MviContext {
+    val testScope: TestScope
+    val turbineContext: TurbineContext
+}
+
+class DefaultMviContext(
+    override val testScope: TestScope,
+    override val turbineContext: TurbineContext,
+) : MviContext
+
+@OptIn(ExperimentalCoroutinesApi::class)
+fun MviContext.advanceUntilIdle() {
+    testScope.advanceUntilIdle()
+}
 
 /**
  * The `turbines` extension function creates a MviTurbines instance for the given MVI ViewModel.
  */
-inline fun <reified STATE, EVENT, EFFECT> TestScope.turbines(
+inline fun <reified STATE, EVENT, EFFECT> MviContext.turbines(
     viewModel: UnidirectionalViewModel<STATE, EVENT, EFFECT>,
 ): MviTurbines<STATE, EFFECT> {
-    return MviTurbines(
-        stateTurbine = viewModel.state.testIn(backgroundScope),
-        effectTurbine = viewModel.effect.testIn(backgroundScope),
-    )
+    with(turbineContext) {
+        return MviTurbines(
+            stateTurbine = viewModel.state.testIn(testScope.backgroundScope),
+            effectTurbine = viewModel.effect.testIn(testScope.backgroundScope),
+        )
+    }
 }
 
 /**
  * The `turbinesWithInitialStateCheck` extension function creates a MviTurbines instance for the given MVI ViewModel
  * and ensures that the initial state is emitted.
  */
-suspend inline fun <reified STATE, EVENT, EFFECT> TestScope.turbinesWithInitialStateCheck(
+suspend inline fun <reified STATE, EVENT, EFFECT> MviContext.turbinesWithInitialStateCheck(
     viewModel: UnidirectionalViewModel<STATE, EVENT, EFFECT>,
     initialState: STATE,
 ): MviTurbines<STATE, EFFECT> {
diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt
index 72a688f1d7e..81ee5980285 100644
--- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt
+++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyIncomingServerSettingsViewModelTest.kt
@@ -2,6 +2,7 @@ package app.k9mail.feature.account.edit.ui.server.settings.modify
 
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.entity.AccountState
@@ -17,7 +18,6 @@ import com.fsck.k9.mail.AuthType
 import com.fsck.k9.mail.ServerSettings
 import com.fsck.k9.mail.store.imap.ImapStoreSettings
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -27,7 +27,7 @@ class ModifyIncomingServerSettingsViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should load account state from use case`() = runTest {
+    fun `should load account state from use case`() = runMviTest {
         val accountUuid = "accountUuid"
         val accountState = AccountState(
             uuid = "accountUuid",
diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt
index 73bd9d0b0f8..78a0142feef 100644
--- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt
+++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/modify/ModifyOutgoingServerSettingsViewModelTest.kt
@@ -2,6 +2,7 @@ package app.k9mail.feature.account.edit.ui.server.settings.modify
 
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.entity.AccountState
@@ -16,7 +17,6 @@ import assertk.assertions.isEqualTo
 import com.fsck.k9.mail.AuthType
 import com.fsck.k9.mail.ServerSettings
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -26,7 +26,7 @@ class ModifyOutgoingServerSettingsViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should load account state from use case`() = runTest {
+    fun `should load account state from use case`() = runMviTest {
         val accountUuid = "accountUuid"
         val accountState = AccountState(
             uuid = "accountUuid",
diff --git a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt
index f98b5dc6d03..c9a024a7079 100644
--- a/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt
+++ b/feature/account/edit/src/test/kotlin/app/k9mail/feature/account/edit/ui/server/settings/save/BaseSaveServerSettingsViewModelTest.kt
@@ -3,6 +3,7 @@ package app.k9mail.feature.account.edit.ui.server.settings.save
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndEffectTurbineConsumed
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndStateTurbineConsumed
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.edit.domain.AccountEditDomainContract
 import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSettingsContract.Effect
@@ -12,7 +13,6 @@ import app.k9mail.feature.account.edit.ui.server.settings.save.SaveServerSetting
 import assertk.assertThat
 import assertk.assertions.isEqualTo
 import assertk.assertions.isNotNull
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -22,7 +22,7 @@ class BaseSaveServerSettingsViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should save server settings when SaveServerSettings event received and emit NavigateNext`() = runTest {
+    fun `should save server settings when SaveServerSettings event received and emit NavigateNext`() = runMviTest {
         var recordedAccountUuid: String? = null
         var recordedIsIncoming: Boolean? = null
         val testSubject = TestSaveServerSettingsViewModel(
@@ -49,7 +49,7 @@ class BaseSaveServerSettingsViewModelTest {
     }
 
     @Test
-    fun `should set error state when save settings failed`() = runTest {
+    fun `should set error state when save settings failed`() = runMviTest {
         val testSubject = TestSaveServerSettingsViewModel(
             accountUuid = ACCOUNT_UUID,
             saveServerSettings = { _, _ ->
@@ -71,7 +71,7 @@ class BaseSaveServerSettingsViewModelTest {
     }
 
     @Test
-    fun `should allow NavigateBack when error and not loading`() = runTest {
+    fun `should allow NavigateBack when error and not loading`() = runMviTest {
         val failure = Failure.SaveServerSettingsFailed("Test exception")
         val testSubject = TestSaveServerSettingsViewModel(
             accountUuid = ACCOUNT_UUID,
diff --git a/feature/account/oauth/src/test/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthViewModelTest.kt b/feature/account/oauth/src/test/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthViewModelTest.kt
index daf97c5a617..f17c15fc344 100644
--- a/feature/account/oauth/src/test/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthViewModelTest.kt
+++ b/feature/account/oauth/src/test/kotlin/app/k9mail/feature/account/oauth/ui/AccountOAuthViewModelTest.kt
@@ -2,8 +2,9 @@ package app.k9mail.feature.account.oauth.ui
 
 import android.app.Activity
 import android.content.Intent
-import app.cash.turbine.testIn
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
+import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.domain.entity.AuthorizationState
 import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
 import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
@@ -12,10 +13,8 @@ import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Error
 import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
 import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
 import assertk.assertThat
-import assertk.assertions.assertThatAndTurbinesConsumed
 import assertk.assertions.isEqualTo
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,329 +27,182 @@ class AccountOAuthViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should change state when google hostname found on initState`() = runTest {
+    fun `should change state when google hostname found on initState`() = runMviTest {
         val testSubject = createTestSubject(
             isGoogleSignIn = true,
         )
-
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State())
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
         testSubject.initState(defaultState)
 
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(defaultState.copy(isGoogleSignIn = true))
-        }
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
+            defaultState.copy(isGoogleSignIn = true),
+        )
     }
 
     @Test
-    fun `should not change state when no google hostname found on initState`() = runTest {
+    fun `should not change state when no google hostname found on initState`() = runMviTest {
         val testSubject = createTestSubject(
             isGoogleSignIn = false,
         )
-
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State())
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
         testSubject.initState(defaultState)
 
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(defaultState.copy(isGoogleSignIn = false))
-        }
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
+            defaultState.copy(isGoogleSignIn = false),
+        )
     }
 
     @Test
-    fun `should launch OAuth when SignInClicked event received`() = runTest {
+    fun `should launch OAuth when SignInClicked event received`() = runMviTest {
         val initialState = defaultState
         val testSubject = createTestSubject(initialState = initialState)
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(initialState)
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
         testSubject.event(Event.SignInClicked)
 
-        assertThatAndTurbinesConsumed(
-            actual = effectTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(Effect.LaunchOAuth(intent))
-        }
+        assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(
+            Effect.LaunchOAuth(intent),
+        )
     }
 
     @Test
-    fun `should show error when SignInClicked event received and OAuth is not supported`() = runTest {
+    fun `should show error when SignInClicked event received and OAuth is not supported`() = runMviTest {
         val initialState = defaultState
         val testSubject = createTestSubject(
             authorizationIntentResult = AuthorizationIntentResult.NotSupported,
             initialState = initialState,
         )
-
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(initialState)
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
         testSubject.event(Event.SignInClicked)
 
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(initialState.copy(error = Error.NotSupported))
-        }
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
+            initialState.copy(error = Error.NotSupported),
+        )
     }
 
     @Test
-    fun `should remove error and launch OAuth when OnRetryClicked event received`() = runTest {
+    fun `should remove error and launch OAuth when OnRetryClicked event received`() = runMviTest {
         val initialState = defaultState.copy(
             error = Error.NotSupported,
         )
         val testSubject = createTestSubject(initialState = initialState)
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(initialState)
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
         testSubject.event(Event.OnRetryClicked)
 
-        assertThat(stateTurbine.awaitItem()).isEqualTo(
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
             initialState.copy(error = null),
         )
 
-        assertThatAndTurbinesConsumed(
-            actual = effectTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(Effect.LaunchOAuth(intent))
-        }
+        assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(
+            Effect.LaunchOAuth(intent),
+        )
     }
 
     @Test
-    fun `should finish OAuth sign in when onOAuthResult received with success`() = runTest {
+    fun `should finish OAuth sign in when onOAuthResult received with success`() = runMviTest {
         val initialState = defaultState
         val authorizationState = AuthorizationState(value = "state")
         val testSubject = createTestSubject(
             authorizationResult = AuthorizationResult.Success(authorizationState),
             initialState = initialState,
         )
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(initialState)
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
         testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
 
         val loadingState = initialState.copy(isLoading = true)
 
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(loadingState)
-        }
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(loadingState)
 
         val successState = loadingState.copy(
             isLoading = false,
         )
 
-        assertThat(stateTurbine.awaitItem()).isEqualTo(successState)
-
-        assertThatAndTurbinesConsumed(
-            actual = effectTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(Effect.NavigateNext(authorizationState))
-        }
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(successState)
+        assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(
+            Effect.NavigateNext(authorizationState),
+        )
     }
 
     @Test
-    fun `should set error state when onOAuthResult received with canceled`() = runTest {
+    fun `should set error state when onOAuthResult received with canceled`() = runMviTest {
         val initialState = defaultState
         val testSubject = createTestSubject(
             authorizationResult = AuthorizationResult.Canceled,
             initialState = initialState,
         )
-        val stateTurbine = testSubject.state.testIn(backgroundScope)
-        val effectTurbine = testSubject.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(initialState)
-        }
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
         testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_CANCELED, data = intent))
 
-        val failureState = initialState.copy(
-            error = Error.Canceled,
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
+            initialState.copy(
+                error = Error.Canceled,
+            ),
         )
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(failureState)
-        }
     }
 
     @Test
     fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is cancelled`() =
-        runTest {
+        runMviTest {
             val initialState = defaultState
             val testSubject = createTestSubject(
                 authorizationResult = AuthorizationResult.Canceled,
                 initialState = initialState,
             )
-            val stateTurbine = testSubject.state.testIn(backgroundScope)
-            val effectTurbine = testSubject.effect.testIn(backgroundScope)
-            val turbines = listOf(stateTurbine, effectTurbine)
-
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(initialState)
-            }
+            val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
             testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
 
             val loadingState = initialState.copy(isLoading = true)
 
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(loadingState)
-            }
+            assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(loadingState)
 
             val failureState = loadingState.copy(
                 isLoading = false,
                 error = Error.Canceled,
             )
 
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(failureState)
-            }
+            assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(failureState)
         }
 
     @Test
     fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is failure`() =
-        runTest {
+        runMviTest {
             val initialState = defaultState
             val failure = Exception("failure")
             val testSubject = createTestSubject(
                 authorizationResult = AuthorizationResult.Failure(failure),
                 initialState = initialState,
             )
-            val stateTurbine = testSubject.state.testIn(backgroundScope)
-            val effectTurbine = testSubject.effect.testIn(backgroundScope)
-            val turbines = listOf(stateTurbine, effectTurbine)
-
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(initialState)
-            }
+            val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
             testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
 
             val loadingState = initialState.copy(isLoading = true)
 
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(loadingState)
-            }
+            assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(loadingState)
 
             val failureState = loadingState.copy(
                 isLoading = false,
                 error = Error.Unknown(failure),
             )
 
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(failureState)
-            }
+            assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(failureState)
         }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val viewModel = createTestSubject()
-        val stateTurbine = viewModel.state.testIn(backgroundScope)
-        val effectTurbine = viewModel.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State())
-        }
+        val turbines = turbinesWithInitialStateCheck(viewModel, State())
 
         viewModel.event(Event.OnBackClicked)
 
-        assertThatAndTurbinesConsumed(
-            actual = effectTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(Effect.NavigateBack)
-        }
+        assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
     }
 
     private companion object {
diff --git a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt
index 192d4f64713..015d8ddeab3 100644
--- a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt
+++ b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/incoming/IncomingServerSettingsViewModelTest.kt
@@ -5,6 +5,7 @@ import app.k9mail.core.common.domain.usecase.validation.ValidationResult
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
 import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.AccountDomainContract
@@ -26,7 +27,6 @@ import assertk.assertions.isEqualTo
 import com.fsck.k9.mail.AuthType
 import com.fsck.k9.mail.ServerSettings
 import com.fsck.k9.mail.store.imap.ImapStoreSettings
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -36,7 +36,7 @@ class IncomingServerSettingsViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should load account setup state when LoadAccountState event is received`() = runTest {
+    fun `should load account setup state when LoadAccountState event is received`() = runMviTest {
         val accountState = AccountState(
             emailAddress = "test@example.com",
             incomingServerSettings = ServerSettings(
@@ -93,7 +93,7 @@ class IncomingServerSettingsViewModelTest {
     }
 
     @Test
-    fun `should change protocol, security and port when ProtocolTypeChanged event is received`() = runTest {
+    fun `should change protocol, security and port when ProtocolTypeChanged event is received`() = runMviTest {
         val initialState = State(
             security = ConnectionSecurity.StartTLS,
             port = NumberInputField(value = ConnectionSecurity.StartTLS.toImapDefaultPort()),
@@ -109,24 +109,22 @@ class IncomingServerSettingsViewModelTest {
                 security = ConnectionSecurity.TLS,
                 port = NumberInputField(value = ConnectionSecurity.TLS.toPop3DefaultPort()),
             ),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ServerChanged event is received`() = runTest {
+    fun `should change state when ServerChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.ServerChanged("server"),
             expectedState = State(server = StringInputField(value = "server")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change security and port when SecurityChanged event is received`() = runTest {
+    fun `should change security and port when SecurityChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
@@ -136,120 +134,110 @@ class IncomingServerSettingsViewModelTest {
                 security = ConnectionSecurity.StartTLS,
                 port = NumberInputField(value = ConnectionSecurity.StartTLS.toImapDefaultPort()),
             ),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when PortChanged event is received`() = runTest {
+    fun `should change state when PortChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.PortChanged(456L),
             expectedState = State(port = NumberInputField(value = 456L)),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change authentication type when AuthenticationTypeChanged event is received`() = runTest {
+    fun `should change authentication type when AuthenticationTypeChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.AuthenticationTypeChanged(AuthenticationType.PasswordEncrypted),
             expectedState = State(authenticationType = AuthenticationType.PasswordEncrypted),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when UsernameChanged event is received`() = runTest {
+    fun `should change state when UsernameChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.UsernameChanged("username"),
             expectedState = State(username = StringInputField(value = "username")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when PasswordChanged event is received`() = runTest {
+    fun `should change state when PasswordChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.PasswordChanged("password"),
             expectedState = State(password = StringInputField(value = "password")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ClientCertificateChanged event is received`() = runTest {
+    fun `should change state when ClientCertificateChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.ClientCertificateChanged("clientCertificate"),
             expectedState = State(clientCertificateAlias = "clientCertificate"),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ImapAutoDetectNamespaceChanged event is received`() = runTest {
+    fun `should change state when ImapAutoDetectNamespaceChanged event is received`() = runMviTest {
         val initialState = State(imapAutodetectNamespaceEnabled = true)
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = State(imapAutodetectNamespaceEnabled = true),
             event = Event.ImapAutoDetectNamespaceChanged(false),
             expectedState = State(imapAutodetectNamespaceEnabled = false),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ImapPrefixChanged event is received`() = runTest {
+    fun `should change state when ImapPrefixChanged event is received`() = runMviTest {
         val initialState = State()
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.ImapPrefixChanged("imapPrefix"),
             expectedState = State(imapPrefix = StringInputField(value = "imapPrefix")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ImapUseCompressionChanged event is received`() = runTest {
+    fun `should change state when ImapUseCompressionChanged event is received`() = runMviTest {
         val initialState = State(imapUseCompression = true)
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.ImapUseCompressionChanged(false),
             expectedState = State(imapUseCompression = false),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ImapSendClientInfoChanged event is received`() = runTest {
+    fun `should change state when ImapSendClientInfoChanged event is received`() = runMviTest {
         val initialState = State(imapSendClientInfo = true)
         eventStateTest(
             viewModel = createTestSubject(initialState),
             initialState = initialState,
             event = Event.ImapSendClientInfoChanged(false),
             expectedState = State(imapSendClientInfo = false),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should save state emit effect NavigateNext when OnNextClicked is received and input valid`() = runTest {
+    fun `should save state emit effect NavigateNext when OnNextClicked is received and input valid`() = runMviTest {
         val initialState = State()
         val repository = InMemoryAccountStateRepository()
         val testSubject = createTestSubject(
@@ -303,7 +291,7 @@ class IncomingServerSettingsViewModelTest {
 
     @Test
     fun `should save state and emit effect NavigateNext when OnNextClicked is received and input valid with OAuth`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 authenticationType = AuthenticationType.OAuth2,
             )
@@ -360,7 +348,7 @@ class IncomingServerSettingsViewModelTest {
 
     @Test
     fun `should change state and not emit NavigateNext effect when OnNextClicked event received and input invalid`() =
-        runTest {
+        runMviTest {
             val testSubject = IncomingServerSettingsViewModel(
                 mode = InteractionMode.Create,
                 validator = FakeIncomingServerSettingsValidator(
@@ -389,7 +377,7 @@ class IncomingServerSettingsViewModelTest {
         }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val testSubject = createTestSubject(State())
         val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
diff --git a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModelTest.kt b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModelTest.kt
index c3877b0106b..dadd63776bc 100644
--- a/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModelTest.kt
+++ b/feature/account/server/settings/src/test/kotlin/app/k9mail/feature/account/server/settings/ui/outgoing/OutgoingServerSettingsViewModelTest.kt
@@ -5,6 +5,7 @@ import app.k9mail.core.common.domain.usecase.validation.ValidationResult
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
 import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.AccountDomainContract
@@ -23,7 +24,6 @@ import assertk.assertThat
 import assertk.assertions.isEqualTo
 import com.fsck.k9.mail.AuthType
 import com.fsck.k9.mail.ServerSettings
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -33,7 +33,7 @@ class OutgoingServerSettingsViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should load account setup state when LoadAccountState event is received`() = runTest {
+    fun `should load account setup state when LoadAccountState event is received`() = runMviTest {
         val accountState = AccountState(
             emailAddress = "test@example.com",
             outgoingServerSettings = ServerSettings(
@@ -80,18 +80,17 @@ class OutgoingServerSettingsViewModelTest {
     }
 
     @Test
-    fun `should change state when ServerChanged event is received`() = runTest {
+    fun `should change state when ServerChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
             event = Event.ServerChanged("server"),
             expectedState = State(server = StringInputField(value = "server")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change security and port when SecurityChanged event is received`() = runTest {
+    fun `should change security and port when SecurityChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
@@ -100,67 +99,61 @@ class OutgoingServerSettingsViewModelTest {
                 security = ConnectionSecurity.StartTLS,
                 port = NumberInputField(value = ConnectionSecurity.StartTLS.toSmtpDefaultPort()),
             ),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when PortChanged event is received`() = runTest {
+    fun `should change state when PortChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
             event = Event.PortChanged(456L),
             expectedState = State(port = NumberInputField(value = 456L)),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when AuthenticationTypeChanged event is received`() = runTest {
+    fun `should change state when AuthenticationTypeChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
             event = Event.AuthenticationTypeChanged(AuthenticationType.PasswordEncrypted),
             expectedState = State(authenticationType = AuthenticationType.PasswordEncrypted),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when UsernameChanged event is received`() = runTest {
+    fun `should change state when UsernameChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
             event = Event.UsernameChanged("username"),
             expectedState = State(username = StringInputField(value = "username")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when PasswordChanged event is received`() = runTest {
+    fun `should change state when PasswordChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
             event = Event.PasswordChanged("password"),
             expectedState = State(password = StringInputField(value = "password")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ClientCertificateChanged event is received`() = runTest {
+    fun `should change state when ClientCertificateChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(State()),
             initialState = State(),
             event = Event.ClientCertificateChanged("clientCertificate"),
             expectedState = State(clientCertificateAlias = "clientCertificate"),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should save state and emit effect NavigateNext when OnNextClicked is received and input valid`() = runTest {
+    fun `should save state and emit effect NavigateNext when OnNextClicked is received and input valid`() = runMviTest {
         val initialState = State()
         val repository = InMemoryAccountStateRepository()
         val testSubject = createTestSubject(
@@ -207,7 +200,7 @@ class OutgoingServerSettingsViewModelTest {
 
     @Test
     fun `should save state and emit effect NavigateNext when OnNextClicked is received and input valid with OAuth`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 authenticationType = AuthenticationType.OAuth2,
             )
@@ -256,7 +249,7 @@ class OutgoingServerSettingsViewModelTest {
 
     @Test
     fun `should change state and not emit NavigateNext effect when OnNextClicked event received and input invalid`() =
-        runTest {
+        runMviTest {
             val initialState = State()
             val testSubject = createTestSubject(
                 validator = FakeOutgoingServerSettingsValidator(
@@ -284,7 +277,7 @@ class OutgoingServerSettingsViewModelTest {
         }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val initialState = State()
         val testSubject = createTestSubject(initialState)
         val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
diff --git a/feature/account/server/validation/src/test/kotlin/app/k9mail/feature/account/server/validation/ui/BaseServerValidationViewModelTest.kt b/feature/account/server/validation/src/test/kotlin/app/k9mail/feature/account/server/validation/ui/BaseServerValidationViewModelTest.kt
index af0f8b03d12..d0d74e83ea3 100644
--- a/feature/account/server/validation/src/test/kotlin/app/k9mail/feature/account/server/validation/ui/BaseServerValidationViewModelTest.kt
+++ b/feature/account/server/validation/src/test/kotlin/app/k9mail/feature/account/server/validation/ui/BaseServerValidationViewModelTest.kt
@@ -2,6 +2,7 @@ package app.k9mail.feature.account.server.validation.ui
 
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.AccountDomainContract
@@ -24,7 +25,6 @@ import com.fsck.k9.mail.ConnectionSecurity
 import com.fsck.k9.mail.ServerSettings
 import com.fsck.k9.mail.server.ServerSettingsValidationResult
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -34,7 +34,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should update state when LoadAccountStateAndValidate event received and validate`() = runTest {
+    fun `should update state when LoadAccountStateAndValidate event received and validate`() = runMviTest {
         val accountState = if (isIncomingValidation) {
             AccountState(
                 incomingServerSettings = SERVER_SETTINGS,
@@ -83,7 +83,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     }
 
     @Test
-    fun `should fail when ValidateServerSettings event received and server settings null`() = runTest {
+    fun `should fail when ValidateServerSettings event received and server settings null`() = runMviTest {
         val initialState = State()
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
@@ -102,7 +102,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     }
 
     @Test
-    fun `should validate server settings when ValidateServerSettings event received`() = runTest {
+    fun `should validate server settings when ValidateServerSettings event received`() = runMviTest {
         val initialState = State(
             serverSettings = SERVER_SETTINGS,
         )
@@ -132,7 +132,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     }
 
     @Test
-    fun `should set error state when ValidateServerSettings received and check settings failed`() = runTest {
+    fun `should set error state when ValidateServerSettings received and check settings failed`() = runMviTest {
         val initialState = State(
             serverSettings = SERVER_SETTINGS,
         )
@@ -160,7 +160,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     }
 
     @Test
-    fun `should emit effect NavigateNext when ValidateConfig is successful`() = runTest {
+    fun `should emit effect NavigateNext when ValidateConfig is successful`() = runMviTest {
         val initialState = State(
             serverSettings = SERVER_SETTINGS,
             isSuccess = true,
@@ -181,7 +181,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
@@ -196,7 +196,7 @@ abstract class BaseServerValidationViewModelTest<T : BaseServerValidationViewMod
     }
 
     @Test
-    fun `should clear error and trigger check settings when OnRetryClicked event received`() = runTest {
+    fun `should clear error and trigger check settings when OnRetryClicked event received`() = runMviTest {
         val initialState = State(
             serverSettings = SERVER_SETTINGS,
             error = Error.ServerError("server error"),
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt
index 3aa896a8c5d..2fcfb4a9513 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/AccountAutoDiscoveryViewModelTest.kt
@@ -6,6 +6,7 @@ import app.k9mail.core.common.domain.usecase.validation.ValidationResult
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
 import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.AccountDomainContract
@@ -24,7 +25,6 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon
 import assertk.assertThat
 import assertk.assertions.isEqualTo
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -34,7 +34,7 @@ class AccountAutoDiscoveryViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `should reset state when EmailAddressChanged event is received`() = runTest {
+    fun `should reset state when EmailAddressChanged event is received`() = runMviTest {
         val initialState = State(
             configStep = ConfigStep.PASSWORD,
             emailAddress = StringInputField(value = "email"),
@@ -51,12 +51,11 @@ class AccountAutoDiscoveryViewModelTest {
                 emailAddress = StringInputField(value = "email"),
                 password = StringInputField(),
             ),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when PasswordChanged event is received`() = runTest {
+    fun `should change state when PasswordChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(),
             initialState = State(),
@@ -64,12 +63,11 @@ class AccountAutoDiscoveryViewModelTest {
             expectedState = State(
                 password = StringInputField(value = "password"),
             ),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when ResultApprovalChanged event is received`() = runTest {
+    fun `should change state when ResultApprovalChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = createTestSubject(),
             initialState = State(),
@@ -77,13 +75,12 @@ class AccountAutoDiscoveryViewModelTest {
             expectedState = State(
                 configurationApproved = BooleanInputField(value = true),
             ),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
     fun `should change state to password when OnNextClicked event is received, input valid and discovery loaded`() =
-        runTest {
+        runMviTest {
             val autoDiscoverySettings = AutoDiscoverySettingsFixture.settings
             val initialState = State(
                 configStep = ConfigStep.EMAIL_ADDRESS,
@@ -132,7 +129,7 @@ class AccountAutoDiscoveryViewModelTest {
 
     @Test
     fun `should not change state when OnNextClicked event is received, input valid but discovery failed`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 configStep = ConfigStep.EMAIL_ADDRESS,
                 emailAddress = StringInputField(value = "email"),
@@ -180,7 +177,7 @@ class AccountAutoDiscoveryViewModelTest {
 
     @Test
     fun `should reset error state and change to password step when OnNextClicked event received when having error`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 configStep = ConfigStep.EMAIL_ADDRESS,
                 emailAddress = StringInputField(
@@ -203,45 +200,44 @@ class AccountAutoDiscoveryViewModelTest {
                     ),
                     error = null,
                 ),
-                coroutineScope = backgroundScope,
             )
         }
 
     @Test
-    fun `should not change config step to password when OnNextClicked event is received and input invalid`() = runTest {
-        val initialState = State(
-            configStep = ConfigStep.EMAIL_ADDRESS,
-            emailAddress = StringInputField(value = "invalid email"),
-        )
-        val testSubject = AccountAutoDiscoveryViewModel(
-            validator = FakeAccountAutoDiscoveryValidator(
-                emailAddressAnswer = ValidationResult.Failure(TestError),
-            ),
-            getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
-            oAuthViewModel = FakeAccountOAuthViewModel(),
-            accountStateRepository = InMemoryAccountStateRepository(),
-            initialState = initialState,
-        )
-
-        eventStateTest(
-            viewModel = testSubject,
-            initialState = initialState,
-            event = Event.OnNextClicked,
-            expectedState = State(
+    fun `should not change config step to password when OnNextClicked event is received and input invalid`() =
+        runMviTest {
+            val initialState = State(
                 configStep = ConfigStep.EMAIL_ADDRESS,
-                emailAddress = StringInputField(
-                    value = "invalid email",
-                    error = TestError,
-                    isValid = false,
+                emailAddress = StringInputField(value = "invalid email"),
+            )
+            val testSubject = AccountAutoDiscoveryViewModel(
+                validator = FakeAccountAutoDiscoveryValidator(
+                    emailAddressAnswer = ValidationResult.Failure(TestError),
                 ),
-            ),
-            coroutineScope = backgroundScope,
-        )
-    }
+                getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
+                oAuthViewModel = FakeAccountOAuthViewModel(),
+                accountStateRepository = InMemoryAccountStateRepository(),
+                initialState = initialState,
+            )
+
+            eventStateTest(
+                viewModel = testSubject,
+                initialState = initialState,
+                event = Event.OnNextClicked,
+                expectedState = State(
+                    configStep = ConfigStep.EMAIL_ADDRESS,
+                    emailAddress = StringInputField(
+                        value = "invalid email",
+                        error = TestError,
+                        isValid = false,
+                    ),
+                ),
+            )
+        }
 
     @Test
     fun `should save state and emit NavigateNext when OnNextClicked received in password step with valid input`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 configStep = ConfigStep.PASSWORD,
                 emailAddress = StringInputField(value = "email"),
@@ -305,7 +301,7 @@ class AccountAutoDiscoveryViewModelTest {
 
     @Test
     fun `should not emit NavigateNext when OnNextClicked received in password step with invalid input`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 configStep = ConfigStep.PASSWORD,
                 emailAddress = StringInputField(value = "email"),
@@ -352,7 +348,7 @@ class AccountAutoDiscoveryViewModelTest {
         }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event is received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event is received`() = runMviTest {
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
@@ -368,7 +364,7 @@ class AccountAutoDiscoveryViewModelTest {
 
     @Test
     fun `should change config step to email address when OnBackClicked event is received in password config step`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 configStep = ConfigStep.PASSWORD,
                 emailAddress = StringInputField(value = "email"),
@@ -394,7 +390,7 @@ class AccountAutoDiscoveryViewModelTest {
 
     @Test
     fun `should reset error state when OnBackClicked event received when having error and in email address step`() =
-        runTest {
+        runMviTest {
             val initialState = State(
                 configStep = ConfigStep.EMAIL_ADDRESS,
                 emailAddress = StringInputField(
@@ -417,12 +413,11 @@ class AccountAutoDiscoveryViewModelTest {
                     ),
                     error = null,
                 ),
-                coroutineScope = backgroundScope,
             )
         }
 
     @Test
-    fun `should emit NavigateNext effect when OnEditConfigurationClicked event is received`() = runTest {
+    fun `should emit NavigateNext effect when OnEditConfigurationClicked event is received`() = runMviTest {
         val initialState = State(
             autoDiscoverySettings = AutoDiscoverySettingsFixture.settings,
         )
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/createaccount/CreateAccountViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/createaccount/CreateAccountViewModelTest.kt
index 6b7b4682bd1..677968e4705 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/createaccount/CreateAccountViewModelTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/createaccount/CreateAccountViewModelTest.kt
@@ -1,8 +1,10 @@
 package app.k9mail.feature.account.setup.ui.createaccount
 
-import app.cash.turbine.testIn
+import app.cash.turbine.turbineScope
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
+import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.entity.AccountDisplayOptions
 import app.k9mail.feature.account.common.domain.entity.AccountState
@@ -48,17 +50,14 @@ class CreateAccountViewModelTest {
     }
 
     @Test
-    fun `should change state and emit navigate effect after successfully creating account`() = runTest {
+    fun `should change state and emit navigate effect after successfully creating account`() = runMviTest {
         val accountUuid = "accountUuid"
         fakeCreateAccount.result = AccountCreatorResult.Success(accountUuid)
+        val turbines = turbinesWithInitialStateCheck(createAccountViewModel, State(isLoading = true, error = null))
 
-        eventStateTest(
-            viewModel = createAccountViewModel,
-            initialState = State(isLoading = true, error = null),
-            event = Event.CreateAccount,
-            expectedState = State(isLoading = false, error = null),
-            coroutineScope = backgroundScope,
-        )
+        createAccountViewModel.event(Event.CreateAccount)
+
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(State(isLoading = false, error = null))
 
         assertThat(fakeCreateAccount.recordedInvocations).containsExactly(
             AccountState(
@@ -72,12 +71,11 @@ class CreateAccountViewModelTest {
             ),
         )
 
-        val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
-        assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateNext(AccountUuid(accountUuid)))
+        assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(Effect.NavigateNext(AccountUuid(accountUuid)))
     }
 
     @Test
-    fun `should change state when creating account has failed`() = runTest {
+    fun `should change state when creating account has failed`() = runMviTest {
         val errorResult = AccountCreatorResult.Error("something went wrong")
         fakeCreateAccount.result = errorResult
 
@@ -86,39 +84,44 @@ class CreateAccountViewModelTest {
             initialState = State(isLoading = true, error = null),
             event = Event.CreateAccount,
             expectedState = State(isLoading = false, error = errorResult),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
     fun `should ignore OnBackClicked event when in loading state`() = runTest {
-        val effectTurbine = createAccountViewModel.effect.testIn(scope = backgroundScope)
+        turbineScope {
+            val effectTurbine = createAccountViewModel.effect.testIn(scope = backgroundScope)
 
-        createAccountViewModel.event(Event.OnBackClicked)
+            createAccountViewModel.event(Event.OnBackClicked)
 
-        effectTurbine.ensureAllEventsConsumed()
+            effectTurbine.ensureAllEventsConsumed()
+        }
     }
 
     @Test
     fun `should emit NavigateBack effect when OnBackClicked event was received while in success state`() = runTest {
-        fakeCreateAccount.result = AccountCreatorResult.Success("accountUuid")
-        createAccountViewModel.event(Event.CreateAccount)
-        val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
+        turbineScope {
+            fakeCreateAccount.result = AccountCreatorResult.Success("accountUuid")
+            createAccountViewModel.event(Event.CreateAccount)
+            val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
 
-        createAccountViewModel.event(Event.OnBackClicked)
+            createAccountViewModel.event(Event.OnBackClicked)
 
-        assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
+            assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
+        }
     }
 
     @Test
     fun `should emit NavigateBack effect when OnBackClicked event was received while in error state`() = runTest {
-        fakeCreateAccount.result = AccountCreatorResult.Error("something went wrong")
-        createAccountViewModel.event(Event.CreateAccount)
-        val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
+        turbineScope {
+            fakeCreateAccount.result = AccountCreatorResult.Error("something went wrong")
+            createAccountViewModel.event(Event.CreateAccount)
+            val effectTurbine = createAccountViewModel.effect.testIn(backgroundScope)
 
-        createAccountViewModel.event(Event.OnBackClicked)
+            createAccountViewModel.event(Event.OnBackClicked)
 
-        assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
+            assertThat(effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
+        }
     }
 
     private companion object {
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/display/DisplayOptionsViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/display/DisplayOptionsViewModelTest.kt
index ff7074cd81c..68c018c82ee 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/display/DisplayOptionsViewModelTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/display/DisplayOptionsViewModelTest.kt
@@ -1,19 +1,18 @@
 package app.k9mail.feature.account.setup.ui.options.display
 
-import app.cash.turbine.testIn
 import app.k9mail.core.common.domain.usecase.validation.ValidationError
 import app.k9mail.core.common.domain.usecase.validation.ValidationResult
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
+import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.input.StringInputField
 import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Effect
 import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.Event
 import app.k9mail.feature.account.setup.ui.options.display.DisplayOptionsContract.State
 import assertk.assertThat
-import assertk.assertions.assertThatAndTurbinesConsumed
 import assertk.assertions.isEqualTo
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -30,56 +29,44 @@ class DisplayOptionsViewModelTest {
     )
 
     @Test
-    fun `should change state when OnAccountNameChanged event is received`() = runTest {
+    fun `should change state when OnAccountNameChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = testSubject,
             initialState = State(),
             event = Event.OnAccountNameChanged("accountName"),
             expectedState = State(accountName = StringInputField(value = "accountName")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when OnDisplayNameChanged event is received`() = runTest {
+    fun `should change state when OnDisplayNameChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = testSubject,
             initialState = State(),
             event = Event.OnDisplayNameChanged("displayName"),
             expectedState = State(displayName = StringInputField(value = "displayName")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when OnEmailSignatureChanged event is received`() = runTest {
+    fun `should change state when OnEmailSignatureChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = testSubject,
             initialState = State(),
             event = Event.OnEmailSignatureChanged("emailSignature"),
             expectedState = State(emailSignature = StringInputField(value = "emailSignature")),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
     fun `should change state and emit NavigateNext effect when OnNextClicked event received and input valid`() =
-        runTest {
+        runMviTest {
             val viewModel = testSubject
-            val stateTurbine = viewModel.state.testIn(backgroundScope)
-            val effectTurbine = viewModel.effect.testIn(backgroundScope)
-            val turbines = listOf(stateTurbine, effectTurbine)
-
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(State())
-            }
+            val turbines = turbinesWithInitialStateCheck(viewModel, State())
 
             viewModel.event(Event.OnNextClicked)
 
-            assertThat(stateTurbine.awaitItem()).isEqualTo(
+            assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
                 State(
                     accountName = StringInputField(value = "", isValid = true),
                     displayName = StringInputField(value = "", isValid = true),
@@ -87,17 +74,12 @@ class DisplayOptionsViewModelTest {
                 ),
             )
 
-            assertThatAndTurbinesConsumed(
-                actual = effectTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(Effect.NavigateNext)
-            }
+            assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(Effect.NavigateNext)
         }
 
     @Test
     fun `should change state and not emit effect when OnNextClicked event received and input invalid`() =
-        runTest {
+        runMviTest {
             val viewModel = DisplayOptionsViewModel(
                 validator = FakeDisplayOptionsValidator(
                     accountNameAnswer = ValidationResult.Failure(TestError),
@@ -105,80 +87,40 @@ class DisplayOptionsViewModelTest {
                 accountStateRepository = InMemoryAccountStateRepository(),
                 accountOwnerNameProvider = accountOwnerNameProvider,
             )
-            val stateTurbine = viewModel.state.testIn(backgroundScope)
-            val effectTurbine = viewModel.effect.testIn(backgroundScope)
-            val turbines = listOf(stateTurbine, effectTurbine)
-
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(State())
-            }
+            val turbines = turbinesWithInitialStateCheck(viewModel, State())
 
             viewModel.event(Event.OnNextClicked)
 
-            assertThatAndTurbinesConsumed(
-                actual = stateTurbine.awaitItem(),
-                turbines = turbines,
-            ) {
-                isEqualTo(
-                    State(
-                        accountName = StringInputField(value = "", error = TestError, isValid = false),
-                        displayName = StringInputField(value = "", isValid = true),
-                        emailSignature = StringInputField(value = "", isValid = true),
-                    ),
-                )
-            }
+            assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
+                State(
+                    accountName = StringInputField(value = "", error = TestError, isValid = false),
+                    displayName = StringInputField(value = "", isValid = true),
+                    emailSignature = StringInputField(value = "", isValid = true),
+                ),
+            )
         }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val viewModel = testSubject
-        val stateTurbine = viewModel.state.testIn(backgroundScope)
-        val effectTurbine = viewModel.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State())
-        }
+        val turbines = turbinesWithInitialStateCheck(viewModel, State())
 
         viewModel.event(Event.OnBackClicked)
 
-        assertThatAndTurbinesConsumed(
-            actual = effectTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(Effect.NavigateBack)
-        }
+        assertThat(turbines.effectTurbine.awaitItem()).isEqualTo(Effect.NavigateBack)
     }
 
     @Test
-    fun `should set owner name when LoadAccountState event received`() = runTest {
+    fun `should set owner name when LoadAccountState event received`() = runMviTest {
         accountOwnerNameProvider.ownerName = "Alice Example"
         val viewModel = testSubject
-        val stateTurbine = viewModel.state.testIn(backgroundScope)
-        val effectTurbine = viewModel.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State())
-        }
+        val turbines = turbinesWithInitialStateCheck(viewModel, State())
 
         viewModel.event(Event.LoadAccountState)
 
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State(displayName = StringInputField("Alice Example")))
-        }
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(
+            State(displayName = StringInputField("Alice Example")),
+        )
     }
 
     private object TestError : ValidationError
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/sync/SyncOptionsViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/sync/SyncOptionsViewModelTest.kt
index 035e5739d3a..4cecd96149c 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/sync/SyncOptionsViewModelTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/options/sync/SyncOptionsViewModelTest.kt
@@ -1,9 +1,9 @@
 package app.k9mail.feature.account.setup.ui.options.sync
 
-import app.cash.turbine.testIn
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndEffectTurbineConsumed
 import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.entity.AccountState
@@ -14,9 +14,7 @@ import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Effe
 import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.Event
 import app.k9mail.feature.account.setup.ui.options.sync.SyncOptionsContract.State
 import assertk.assertThat
-import assertk.assertions.assertThatAndTurbinesConsumed
 import assertk.assertions.isEqualTo
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -30,41 +28,38 @@ class SyncOptionsViewModelTest {
     )
 
     @Test
-    fun `should change state when OnCheckFrequencyChanged event is received`() = runTest {
+    fun `should change state when OnCheckFrequencyChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = testSubject,
             initialState = State(),
             event = Event.OnCheckFrequencyChanged(EmailCheckFrequency.EVERY_12_HOURS),
             expectedState = State(checkFrequency = EmailCheckFrequency.EVERY_12_HOURS),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when OnMessageDisplayCountChanged event is received`() = runTest {
+    fun `should change state when OnMessageDisplayCountChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = testSubject,
             initialState = State(),
             event = Event.OnMessageDisplayCountChanged(EmailDisplayCount.MESSAGES_1000),
             expectedState = State(messageDisplayCount = EmailDisplayCount.MESSAGES_1000),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
-    fun `should change state when OnShowNotificationChanged event is received`() = runTest {
+    fun `should change state when OnShowNotificationChanged event is received`() = runMviTest {
         eventStateTest(
             viewModel = testSubject,
             initialState = State(),
             event = Event.OnShowNotificationChanged(false),
             expectedState = State(showNotification = false),
-            coroutineScope = backgroundScope,
         )
     }
 
     @Test
     fun `should store state and emit NavigateNext effect when OnNextClicked event received and input valid`() =
-        runTest {
+        runMviTest {
             val accountStateRepository = InMemoryAccountStateRepository()
             val initialState = State(
                 checkFrequency = EmailCheckFrequency.EVERY_HOUR,
@@ -98,26 +93,12 @@ class SyncOptionsViewModelTest {
         }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val viewModel = testSubject
-        val stateTurbine = viewModel.state.testIn(backgroundScope)
-        val effectTurbine = viewModel.effect.testIn(backgroundScope)
-        val turbines = listOf(stateTurbine, effectTurbine)
-
-        assertThatAndTurbinesConsumed(
-            actual = stateTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(State())
-        }
+        val turbines = turbinesWithInitialStateCheck(viewModel, State())
 
         viewModel.event(Event.OnBackClicked)
 
-        assertThatAndTurbinesConsumed(
-            actual = effectTurbine.awaitItem(),
-            turbines = turbines,
-        ) {
-            isEqualTo(Effect.NavigateBack)
-        }
+        assertThat(turbines.awaitEffectItem()).isEqualTo(Effect.NavigateBack)
     }
 }
diff --git a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/specialfolders/SpecialFoldersViewModelTest.kt b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/specialfolders/SpecialFoldersViewModelTest.kt
index 483bb400546..cbed2581248 100644
--- a/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/specialfolders/SpecialFoldersViewModelTest.kt
+++ b/feature/account/setup/src/test/kotlin/app/k9mail/feature/account/setup/ui/specialfolders/SpecialFoldersViewModelTest.kt
@@ -5,6 +5,7 @@ import app.k9mail.core.common.domain.usecase.validation.ValidationResult
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndEffectTurbineConsumed
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndStateTurbineConsumed
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.account.common.data.InMemoryAccountStateRepository
 import app.k9mail.feature.account.common.domain.AccountDomainContract
@@ -25,7 +26,6 @@ import com.fsck.k9.mail.folders.FolderFetcherException
 import com.fsck.k9.mail.folders.FolderServerId
 import com.fsck.k9.mail.folders.RemoteFolder
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 
@@ -36,7 +36,7 @@ class SpecialFoldersViewModelTest {
 
     @Test
     fun `should load folders, validate and save successfully when LoadSpecialFolders event received and setup valid`() =
-        runTest {
+        runMviTest {
             val accountStateRepository = InMemoryAccountStateRepository()
             val initialState = State(
                 isLoading = true,
@@ -91,7 +91,7 @@ class SpecialFoldersViewModelTest {
         }
 
     @Test
-    fun `should load folders and validate unsuccessful when LoadSpecialFolders event received`() = runTest {
+    fun `should load folders and validate unsuccessful when LoadSpecialFolders event received`() = runMviTest {
         val accountStateRepository = InMemoryAccountStateRepository()
         val initialState = State(
             isLoading = true,
@@ -135,7 +135,7 @@ class SpecialFoldersViewModelTest {
     }
 
     @Test
-    fun `should change to error state when LoadSpecialFolders fails with loading folder failure`() = runTest {
+    fun `should change to error state when LoadSpecialFolders fails with loading folder failure`() = runMviTest {
         val initialState = State(
             isLoading = true,
         )
@@ -164,7 +164,7 @@ class SpecialFoldersViewModelTest {
     }
 
     @Test
-    fun `should delegate form events to form view model`() = runTest {
+    fun `should delegate form events to form view model`() = runMviTest {
         val formUiModel = FakeSpecialFoldersFormUiModel()
         val testSubject = createTestSubject(
             formUiModel = formUiModel,
@@ -186,7 +186,7 @@ class SpecialFoldersViewModelTest {
     }
 
     @Test
-    fun `should save form data and emit NavigateNext effect when OnNextClicked event received`() = runTest {
+    fun `should save form data and emit NavigateNext effect when OnNextClicked event received`() = runMviTest {
         val initialState = State(isManualSetup = true)
         val accountStateRepository = InMemoryAccountStateRepository()
         val testSubject = createTestSubject(
@@ -217,7 +217,7 @@ class SpecialFoldersViewModelTest {
     }
 
     @Test
-    fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
+    fun `should emit NavigateBack effect when OnBackClicked event received`() = runMviTest {
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
@@ -229,7 +229,7 @@ class SpecialFoldersViewModelTest {
     }
 
     @Test
-    fun `should show form when OnRetryClicked event received`() = runTest {
+    fun `should show form when OnRetryClicked event received`() = runMviTest {
         val initialState = State(error = SpecialFoldersContract.Failure.LoadFoldersFailed("irrelevant"))
         val testSubject = createTestSubject(initialState = initialState)
         val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
diff --git a/feature/migration/qrcode/src/test/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerViewModelTest.kt b/feature/migration/qrcode/src/test/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerViewModelTest.kt
index 61b5ee421a4..bdb73183ee9 100644
--- a/feature/migration/qrcode/src/test/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerViewModelTest.kt
+++ b/feature/migration/qrcode/src/test/kotlin/app/k9mail/feature/migration/qrcode/ui/QrCodeScannerViewModelTest.kt
@@ -2,7 +2,9 @@ package app.k9mail.feature.migration.qrcode.ui
 
 import android.app.Application
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
+import app.k9mail.core.ui.compose.testing.mvi.MviContext
 import app.k9mail.core.ui.compose.testing.mvi.MviTurbines
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.migration.qrcode.domain.QrCodeDomainContract.UseCase
 import app.k9mail.feature.migration.qrcode.domain.usecase.QrCodePayloadReader
@@ -17,8 +19,6 @@ import assertk.assertions.isInstanceOf
 import assertk.assertions.isNotNull
 import assertk.assertions.isNull
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -33,8 +33,8 @@ class QrCodeScannerViewModelTest {
     val mainDispatcherRule = MainDispatcherRule()
 
     @Test
-    fun `user grants camera permission`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user grants camera permission`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             userGrantsCameraPermission()
 
@@ -43,8 +43,8 @@ class QrCodeScannerViewModelTest {
     }
 
     @Test
-    fun `user denies camera permission`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user denies camera permission`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             userDeniesCameraPermission()
 
@@ -53,8 +53,8 @@ class QrCodeScannerViewModelTest {
     }
 
     @Test
-    fun `user grants camera permission via android app settings`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user grants camera permission via android app settings`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             systemDeniesCameraPermission()
 
@@ -67,8 +67,8 @@ class QrCodeScannerViewModelTest {
     }
 
     @Test
-    fun `user successfully scans one QR code`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user successfully scans one QR code`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             systemGrantsCameraPermission()
 
@@ -82,8 +82,8 @@ class QrCodeScannerViewModelTest {
     }
 
     @Test
-    fun `user successfully scans two QR codes`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user successfully scans two QR codes`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             systemGrantsCameraPermission()
 
@@ -100,8 +100,8 @@ class QrCodeScannerViewModelTest {
     }
 
     @Test
-    fun `user clicks Done button after one of two QR codes was scanned`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user clicks Done button after one of two QR codes was scanned`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             systemGrantsCameraPermission()
 
@@ -116,8 +116,8 @@ class QrCodeScannerViewModelTest {
     }
 
     @Test
-    fun `user clicks Done button without any QR codes having been successfully scanned`() = runTest {
-        with(QrCodeScannerScreenRobot(testScope = this)) {
+    fun `user clicks Done button without any QR codes having been successfully scanned`() = runMviTest {
+        with(QrCodeScannerScreenRobot(mviContext = this)) {
             startScreen()
             systemGrantsCameraPermission()
 
@@ -130,7 +130,7 @@ class QrCodeScannerViewModelTest {
 }
 
 private class QrCodeScannerScreenRobot(
-    private val testScope: TestScope,
+    private val mviContext: MviContext,
 ) {
     private val qrCodeSettingsWriter = FakeQrCodeSettingsWriter()
     private val viewModel = QrCodeScannerViewModel(
@@ -148,7 +148,7 @@ private class QrCodeScannerScreenRobot(
     private val initialState = State()
 
     suspend fun startScreen() {
-        turbines = testScope.turbinesWithInitialStateCheck(viewModel, initialState)
+        turbines = mviContext.turbinesWithInitialStateCheck(viewModel, initialState)
 
         viewModel.event(Event.StartScreen)
 
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt
index 6cdc88e543c..493fb8c8c13 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt
@@ -3,9 +3,9 @@ package app.k9mail.feature.navigation.drawer.ui
 import app.k9mail.core.mail.folder.api.Folder
 import app.k9mail.core.mail.folder.api.FolderType
 import app.k9mail.core.ui.compose.testing.MainDispatcherRule
+import app.k9mail.core.ui.compose.testing.mvi.advanceUntilIdle
 import app.k9mail.core.ui.compose.testing.mvi.assertThatAndEffectTurbineConsumed
-import app.k9mail.core.ui.compose.testing.mvi.assertThatAndStateTurbineConsumed
-import app.k9mail.core.ui.compose.testing.mvi.eventStateTest
+import app.k9mail.core.ui.compose.testing.mvi.runMviTest
 import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
 import app.k9mail.feature.navigation.drawer.NavigationDrawerExternalContract.DrawerConfig
 import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount
@@ -61,7 +61,7 @@ internal class DrawerViewModelTest {
     }
 
     @Test
-    fun `should change loading state when OnSyncAccount event is received`() = runTest {
+    fun `should change loading state when OnSyncAccount event is received`() = runMviTest {
         val initialState = State(
             accounts = listOf(DISPLAY_ACCOUNT).toImmutableList(),
             selectedAccountUuid = DISPLAY_ACCOUNT.uuid,
@@ -74,22 +74,16 @@ internal class DrawerViewModelTest {
                 emit(Result.success(Unit))
             },
         )
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
-        eventStateTest(
-            viewModel = testSubject,
-            initialState = initialState,
-            event = Event.OnSyncAccount,
-            expectedState = initialState.copy(isLoading = true),
-            coroutineScope = backgroundScope,
-        )
-
-        advanceUntilIdle()
+        testSubject.event(Event.OnSyncAccount)
 
-        assertThat(testSubject.state.value.isLoading).isEqualTo(false)
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(initialState.copy(isLoading = true))
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(initialState.copy(isLoading = false))
     }
 
     @Test
-    fun `should skip loading when no account is selected and OnSyncAccount event is received`() = runTest {
+    fun `should skip loading when no account is selected and OnSyncAccount event is received`() = runMviTest {
         val initialState = State(selectedAccountUuid = null)
         var counter = 0
         val testSubject = createTestSubject(
@@ -115,7 +109,7 @@ internal class DrawerViewModelTest {
     }
 
     @Test
-    fun `should skip loading when already loading and OnSyncAccount event received`() = runTest {
+    fun `should skip loading when already loading and OnSyncAccount event received`() = runMviTest {
         val initialState = State(isLoading = true)
         var counter = 0
         val testSubject = createTestSubject(
@@ -140,29 +134,24 @@ internal class DrawerViewModelTest {
     }
 
     @Test
-    fun `should change loading state when OnSyncAllAccounts event is received`() = runTest {
+    fun `should change loading state when OnSyncAllAccounts event is received`() = runMviTest {
         val testSubject = createTestSubject(
             syncAllAccounts = flow {
                 delay(25)
                 emit(Result.success(Unit))
             },
         )
+        val initialState = State(isLoading = false)
+        val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
 
-        eventStateTest(
-            viewModel = testSubject,
-            initialState = State(isLoading = false),
-            event = Event.OnSyncAllAccounts,
-            expectedState = State(isLoading = true),
-            coroutineScope = backgroundScope,
-        )
-
-        advanceUntilIdle()
+        testSubject.event(Event.OnSyncAllAccounts)
 
-        assertThat(testSubject.state.value.isLoading).isEqualTo(false)
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(State(isLoading = true))
+        assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(State(isLoading = false))
     }
 
     @Test
-    fun `should skip loading when already loading and OnSyncAllAccounts event received`() = runTest {
+    fun `should skip loading when already loading and OnSyncAllAccounts event received`() = runMviTest {
         val initialState = State(isLoading = true)
         var counter = 0
         val testSubject = createTestSubject(
@@ -235,7 +224,7 @@ internal class DrawerViewModelTest {
     }
 
     @Test
-    fun `should send OpenAccount effect when OnAccountClick event is received`() = runTest {
+    fun `should send OpenAccount effect when OnAccountClick event is received`() = runMviTest {
         val displayAccounts = createDisplayAccountList(3)
         val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
         val testSubject = createTestSubject(
@@ -307,7 +296,7 @@ internal class DrawerViewModelTest {
     }
 
     @Test
-    fun `should emit OpenFolder effect when OnFolderClick event is received`() = runTest {
+    fun `should emit OpenFolder effect when OnFolderClick event is received`() = runMviTest {
         val displayAccounts = createDisplayAccountList(3)
         val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
         val displayFoldersMap = mapOf(
@@ -340,7 +329,7 @@ internal class DrawerViewModelTest {
 
     @Test
     fun `should emit OpenUnifiedFolder when OnFolderClick event for unified folder is received`() =
-        runTest {
+        runMviTest {
             val displayAccounts = createDisplayAccountList(1)
             val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
             val displayFoldersMap = mapOf(
@@ -373,7 +362,7 @@ internal class DrawerViewModelTest {
         }
 
     @Test
-    fun `should change state when OnAccountSelectorClick event is received`() = runTest {
+    fun `should change state when OnAccountSelectorClick event is received`() = runMviTest {
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
@@ -383,13 +372,11 @@ internal class DrawerViewModelTest {
 
         testSubject.event(Event.OnAccountSelectorClick)
 
-        turbines.assertThatAndStateTurbineConsumed {
-            isEqualTo(State(showAccountSelector = false))
-        }
+        assertThat(turbines.awaitStateItem()).isEqualTo(State(showAccountSelector = false))
     }
 
     @Test
-    fun `should emit OpenManageFolders effect when OnManageFoldersClick event is received`() = runTest {
+    fun `should emit OpenManageFolders effect when OnManageFoldersClick event is received`() = runMviTest {
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, State())
 
@@ -401,7 +388,7 @@ internal class DrawerViewModelTest {
     }
 
     @Test
-    fun `should emit OpenSettings effect when OnSettingsClick event is received`() = runTest {
+    fun `should emit OpenSettings effect when OnSettingsClick event is received`() = runMviTest {
         val testSubject = createTestSubject()
         val turbines = turbinesWithInitialStateCheck(testSubject, State())