From 52891f670d88cd5dd81aa7637b931670da5d3bf7 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Thu, 4 Mar 2021 13:27:04 +0100
Subject: [PATCH] Small refactoring, a review of used scopes for coroutines
 (DEV) #2516

* Small refactoring, a review of used scopes for coroutines.
Use AppScope instead of ViewModelScope, if there is a reasonable expectation that the a screen could be closed prematurely, and we still want an operation scoped to it's viewmodel to complete.

* Fix tests

Co-authored-by: ralfgehrer <mail@ralfgehrer.com>
Co-authored-by: Lukas Lechner <lukas.lechner@sap.com>
---
 .../ContactDiaryEditLocationsFragmentTest.kt  |  2 ++
 .../ContactDiaryEditPersonsFragmentTest.kt    |  2 ++
 .../crash.ui/SettingsCrashReportViewModel.kt  |  3 +--
 ...iskLevelCalculationFragmentCWAViewModel.kt |  2 +-
 .../ContactDiaryEditLocationsViewModel.kt     | 12 +++++++++---
 .../edit/ContactDiaryEditPersonsViewModel.kt  | 15 ++++++++++-----
 .../ContactDiaryAddLocationViewModel.kt       | 18 ++++++++++++------
 .../person/ContactDiaryAddPersonViewModel.kt  | 16 +++++++++++-----
 .../util/viewmodel/CWAViewModel.kt            |  9 +++++++--
 .../ContactDiaryEditLocationsViewModelTest.kt | 19 +++++++++++++------
 .../ContactDiaryEditPersonsViewModelTest.kt   | 19 +++++++++++++------
 11 files changed, 81 insertions(+), 36 deletions(-)

diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditLocationsFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditLocationsFragmentTest.kt
index 195824d42..7bb0ecd50 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditLocationsFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditLocationsFragmentTest.kt
@@ -12,6 +12,7 @@ import io.mockk.MockKAnnotations
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.spyk
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -44,6 +45,7 @@ class ContactDiaryEditLocationsFragmentTest : BaseUITest() {
         MockKAnnotations.init(this, relaxed = true)
         viewModel = spyk(
             ContactDiaryEditLocationsViewModel(
+                TestCoroutineScope(),
                 contactDiaryRepository,
                 TestDispatcherProvider()
             )
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditPersonsFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditPersonsFragmentTest.kt
index 1f0440d76..1e80c7199 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditPersonsFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryEditPersonsFragmentTest.kt
@@ -12,6 +12,7 @@ import io.mockk.MockKAnnotations
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.spyk
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -44,6 +45,7 @@ class ContactDiaryEditPersonsFragmentTest : BaseUITest() {
         MockKAnnotations.init(this, relaxed = true)
         viewModel = spyk(
             ContactDiaryEditPersonsViewModel(
+                TestCoroutineScope(),
                 contactDiaryRepository,
                 TestDispatcherProvider()
             )
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt
index e9a0f09ea..b02e7a18b 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt
@@ -11,7 +11,6 @@ import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.bugreporting.storage.repository.BugRepository
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import kotlinx.coroutines.Dispatchers
 import timber.log.Timber
 
 class SettingsCrashReportViewModel @AssistedInject constructor(
@@ -26,7 +25,7 @@ class SettingsCrashReportViewModel @AssistedInject constructor(
         createBugEventFormattedText(it)
     }
 
-    fun deleteAllCrashReports() = launch(Dispatchers.IO) {
+    fun deleteAllCrashReports() = launch {
         crashReportRepository.clear()
     }
 
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
index 7066eebed..75beae1cd 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
@@ -202,7 +202,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
 
     fun shareExposureWindows() {
         Timber.d("Creating text file for Exposure Windows")
-        launch(dispatcherProvider.IO) {
+        launch {
             val exposureWindows = lastRiskResult.firstOrNull()?.exposureWindows?.map { it.toExposureWindowJson() }
             val fileNameCompatibleTimestamp = timeStamper.nowUTC.toString(
                 DateTimeFormat.forPattern("yyyy-MM-DD-HH-mm-ss")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt
index d18e80939..89e11ab1e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt
@@ -10,18 +10,24 @@ import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.map
 
 class ContactDiaryEditLocationsViewModel @AssistedInject constructor(
+    @AppScope private val appScope: CoroutineScope,
     private val contactDiaryRepository: ContactDiaryRepository,
     dispatcherProvider: DispatcherProvider
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
-    private val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
-        ex.report(ExceptionCategory.INTERNAL, TAG)
+
+    init {
+        launchErrorHandler = CoroutineExceptionHandler { _, ex ->
+            ex.report(ExceptionCategory.INTERNAL, TAG)
+        }
     }
 
     val locationsLiveData = contactDiaryRepository.locations
@@ -40,7 +46,7 @@ class ContactDiaryEditLocationsViewModel @AssistedInject constructor(
     }
 
     fun onDeleteAllConfirmedClick() {
-        launch(coroutineExceptionHandler) {
+        launch(scope = appScope) {
             contactDiaryRepository.deleteAllLocationVisits()
             contactDiaryRepository.deleteAllLocations()
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt
index d648222c2..2fe31146e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt
@@ -10,17 +10,26 @@ import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.map
 
 class ContactDiaryEditPersonsViewModel @AssistedInject constructor(
+    @AppScope private val appScope: CoroutineScope,
     private val contactDiaryRepository: ContactDiaryRepository,
     dispatcherProvider: DispatcherProvider
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
+    init {
+        launchErrorHandler = CoroutineExceptionHandler { _, ex ->
+            ex.report(ExceptionCategory.INTERNAL, TAG)
+        }
+    }
+
     val navigationEvent = SingleLiveEvent<NavigationEvent>()
 
     val personsLiveData = contactDiaryRepository.people
@@ -32,16 +41,12 @@ class ContactDiaryEditPersonsViewModel @AssistedInject constructor(
     val isListVisible = contactDiaryRepository.people.map { it.isNotEmpty() }
         .asLiveData(dispatcherProvider.IO)
 
-    private val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
-        ex.report(ExceptionCategory.INTERNAL, TAG)
-    }
-
     fun onDeleteAllPersonsClick() {
         navigationEvent.postValue(NavigationEvent.ShowDeletionConfirmationDialog)
     }
 
     fun onDeleteAllConfirmedClick() {
-        launch(coroutineExceptionHandler) {
+        launch(scope = appScope) {
             contactDiaryRepository.deleteAllPersonEncounters()
             contactDiaryRepository.deleteAllPeople()
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt
index ca6484156..be3954c33 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt
@@ -11,23 +11,29 @@ import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.map
 import org.joda.time.LocalDate
 
 class ContactDiaryAddLocationViewModel @AssistedInject constructor(
+    @AppScope private val appScope: CoroutineScope,
     dispatcherProvider: DispatcherProvider,
     @Assisted private val addedAt: String?,
     private val contactDiaryRepository: ContactDiaryRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
-    private val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
-        shouldClose.postValue(null)
-        ex.report(ExceptionCategory.INTERNAL, TAG)
+
+    init {
+        launchErrorHandler = CoroutineExceptionHandler { _, ex ->
+            shouldClose.postValue(null)
+            ex.report(ExceptionCategory.INTERNAL, TAG)
+        }
     }
 
     val shouldClose = SingleLiveEvent<Unit>()
@@ -42,7 +48,7 @@ class ContactDiaryAddLocationViewModel @AssistedInject constructor(
         locationName.value = value
     }
 
-    fun addLocation(phoneNumber: String, emailAddress: String) = launch(coroutineExceptionHandler) {
+    fun addLocation(phoneNumber: String, emailAddress: String) = launch(scope = appScope) {
         val location = contactDiaryRepository.addLocation(
             DefaultContactDiaryLocation(
                 locationName = locationName.value,
@@ -63,7 +69,7 @@ class ContactDiaryAddLocationViewModel @AssistedInject constructor(
     }
 
     fun updateLocation(location: ContactDiaryLocationEntity, phoneNumber: String, emailAddress: String) =
-        launch(coroutineExceptionHandler) {
+        launch(scope = appScope) {
             contactDiaryRepository.updateLocation(
                 DefaultContactDiaryLocation(
                     location.locationId,
@@ -75,7 +81,7 @@ class ContactDiaryAddLocationViewModel @AssistedInject constructor(
             shouldClose.postValue(null)
         }
 
-    fun deleteLocation(location: ContactDiaryLocationEntity) = launch(coroutineExceptionHandler) {
+    fun deleteLocation(location: ContactDiaryLocationEntity) = launch(scope = appScope) {
         contactDiaryRepository.locationVisits.firstOrNull()?.forEach {
             if (it.contactDiaryLocation.locationId == location.locationId)
                 contactDiaryRepository.deleteLocationVisit(it)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt
index 34eb68336..6ef9c0ad6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt
@@ -10,23 +10,29 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.map
 import org.joda.time.LocalDate
 
 class ContactDiaryAddPersonViewModel @AssistedInject constructor(
+    @AppScope private val appScope: CoroutineScope,
     dispatcherProvider: DispatcherProvider,
     @Assisted private val addedAt: String?,
     private val contactDiaryRepository: ContactDiaryRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
-    private val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
-        ex.report(ExceptionCategory.INTERNAL, TAG)
+
+    init {
+        launchErrorHandler = CoroutineExceptionHandler { _, ex ->
+            ex.report(ExceptionCategory.INTERNAL, TAG)
+        }
     }
 
     val shouldClose = SingleLiveEvent<Unit>()
@@ -41,7 +47,7 @@ class ContactDiaryAddPersonViewModel @AssistedInject constructor(
         name.value = value
     }
 
-    fun addPerson(phoneNumber: String, emailAddress: String) = launch(coroutineExceptionHandler) {
+    fun addPerson(phoneNumber: String, emailAddress: String) = launch(scope = appScope) {
         val person = contactDiaryRepository.addPerson(
             DefaultContactDiaryPerson(
                 fullName = name.value,
@@ -62,7 +68,7 @@ class ContactDiaryAddPersonViewModel @AssistedInject constructor(
     }
 
     fun updatePerson(person: ContactDiaryPersonEntity, phoneNumber: String, emailAddress: String) =
-        launch(coroutineExceptionHandler) {
+        launch(scope = appScope) {
             contactDiaryRepository.updatePerson(
                 DefaultContactDiaryPerson(
                     person.personId,
@@ -75,7 +81,7 @@ class ContactDiaryAddPersonViewModel @AssistedInject constructor(
             shouldClose.postValue(null)
         }
 
-    fun deletePerson(person: ContactDiaryPersonEntity) = launch(coroutineExceptionHandler) {
+    fun deletePerson(person: ContactDiaryPersonEntity) = launch(scope = appScope) {
         contactDiaryRepository.personEncounters.firstOrNull()?.forEach {
             if (it.contactDiaryPerson.personId == person.personId)
                 contactDiaryRepository.deletePersonEncounter(it)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
index 32217bfaf..eca763ca8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
 import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.launchIn
@@ -20,6 +21,7 @@ abstract class CWAViewModel constructor(
 ) : ViewModel() {
 
     private val tag: String = this::class.simpleName!!
+    var launchErrorHandler: CoroutineExceptionHandler? = null
 
     init {
         Timber.tag(tag).v("Initialized")
@@ -30,13 +32,16 @@ abstract class CWAViewModel constructor(
      * Remember to switch to the main thread if you want to update the UI directly
      */
     fun launch(
+        scope: CoroutineScope = viewModelScope,
         context: CoroutineContext = dispatcherProvider.Default,
+        errorHandler: CoroutineExceptionHandler? = launchErrorHandler,
         block: suspend CoroutineScope.() -> Unit
     ) {
+        val combinedContext = errorHandler?.let { context + it } ?: context
         try {
-            viewModelScope.launch(context = context, block = block)
+            scope.launch(context = combinedContext, block = block)
         } catch (e: CancellationException) {
-            Timber.w(e, "launch()ed coroutine was canceled.")
+            Timber.w(e, "launch()ed coroutine was canceled (scope=%s).", scope)
         }
     }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt
index 6dd141a33..85cec1b37 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt
@@ -12,6 +12,7 @@ import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
@@ -37,10 +38,16 @@ class ContactDiaryEditLocationsViewModelTest {
         MockKAnnotations.init(this)
     }
 
+    fun createInstance() = ContactDiaryEditLocationsViewModel(
+        appScope = TestCoroutineScope(),
+        contactDiaryRepository = contactDiaryRepository,
+        dispatcherProvider = TestDispatcherProvider()
+    )
+
     @Test
     fun testOnDeleteAllLocationsClick() {
         every { contactDiaryRepository.locations } returns MutableStateFlow(locationList)
-        viewModel = ContactDiaryEditLocationsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.navigationEvent.observeForever { }
         viewModel.onDeleteAllLocationsClick()
         viewModel.navigationEvent.value shouldBe ContactDiaryEditLocationsViewModel.NavigationEvent.ShowDeletionConfirmationDialog
@@ -51,7 +58,7 @@ class ContactDiaryEditLocationsViewModelTest {
         coEvery { contactDiaryRepository.deleteAllLocationVisits() } just Runs
         coEvery { contactDiaryRepository.deleteAllLocations() } just Runs
         every { contactDiaryRepository.locations } returns MutableStateFlow(locationList)
-        viewModel = ContactDiaryEditLocationsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.onDeleteAllConfirmedClick()
         coVerify(exactly = 1) {
             contactDiaryRepository.deleteAllLocationVisits()
@@ -62,7 +69,7 @@ class ContactDiaryEditLocationsViewModelTest {
     @Test
     fun testOnEditLocationClick() {
         every { contactDiaryRepository.locations } returns MutableStateFlow(locationList)
-        viewModel = ContactDiaryEditLocationsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.navigationEvent.observeForever { }
         viewModel.onEditLocationClick(location)
         viewModel.navigationEvent.value shouldBe
@@ -72,7 +79,7 @@ class ContactDiaryEditLocationsViewModelTest {
     @Test
     fun testIsButtonEnabled() {
         every { contactDiaryRepository.locations } returns MutableStateFlow(locationList)
-        viewModel = ContactDiaryEditLocationsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.isButtonEnabled.observeForever { }
         viewModel.isButtonEnabled.value shouldBe true
     }
@@ -80,7 +87,7 @@ class ContactDiaryEditLocationsViewModelTest {
     @Test
     fun testIsButtonNotEnabledWhenListIsEmpty() {
         every { contactDiaryRepository.locations } returns MutableStateFlow(emptyList())
-        viewModel = ContactDiaryEditLocationsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.isButtonEnabled.observeForever { }
         viewModel.isButtonEnabled.value shouldBe false
     }
@@ -88,7 +95,7 @@ class ContactDiaryEditLocationsViewModelTest {
     @Test
     fun testLocations() {
         every { contactDiaryRepository.locations } returns MutableStateFlow(locationList)
-        viewModel = ContactDiaryEditLocationsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.locationsLiveData.observeForever { }
         viewModel.locationsLiveData.value shouldBe locationList
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt
index 892d74d8b..babe9784b 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt
@@ -12,6 +12,7 @@ import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
@@ -38,10 +39,16 @@ class ContactDiaryEditPersonsViewModelTest {
         MockKAnnotations.init(this)
     }
 
+    private fun createInstance() = ContactDiaryEditPersonsViewModel(
+        appScope = TestCoroutineScope(),
+        contactDiaryRepository = contactDiaryRepository,
+        dispatcherProvider = TestDispatcherProvider()
+    )
+
     @Test
     fun testOnDeleteAllLocationsClick() {
         every { contactDiaryRepository.people } returns MutableStateFlow(personList)
-        viewModel = ContactDiaryEditPersonsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.navigationEvent.observeForever { }
         viewModel.onDeleteAllPersonsClick()
         viewModel.navigationEvent.value shouldBe ContactDiaryEditPersonsViewModel.NavigationEvent.ShowDeletionConfirmationDialog
@@ -52,7 +59,7 @@ class ContactDiaryEditPersonsViewModelTest {
         coEvery { contactDiaryRepository.deleteAllPeople() } just Runs
         coEvery { contactDiaryRepository.deleteAllPersonEncounters() } just Runs
         every { contactDiaryRepository.people } returns MutableStateFlow(personList)
-        viewModel = ContactDiaryEditPersonsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.onDeleteAllConfirmedClick()
         coVerify(exactly = 1) {
             contactDiaryRepository.deleteAllPeople()
@@ -63,7 +70,7 @@ class ContactDiaryEditPersonsViewModelTest {
     @Test
     fun testOnEditLocationClick() {
         every { contactDiaryRepository.people } returns MutableStateFlow(personList)
-        viewModel = ContactDiaryEditPersonsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.navigationEvent.observeForever { }
         viewModel.onEditPersonClick(person)
         viewModel.navigationEvent.value shouldBe
@@ -73,7 +80,7 @@ class ContactDiaryEditPersonsViewModelTest {
     @Test
     fun testIsButtonEnabled() {
         every { contactDiaryRepository.people } returns MutableStateFlow(personList)
-        viewModel = ContactDiaryEditPersonsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.isButtonEnabled.observeForever { }
         viewModel.isButtonEnabled.value shouldBe true
     }
@@ -81,7 +88,7 @@ class ContactDiaryEditPersonsViewModelTest {
     @Test
     fun testIsButtonNotEnabledWhenListIsEmpty() {
         every { contactDiaryRepository.people } returns MutableStateFlow(emptyList())
-        viewModel = ContactDiaryEditPersonsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.isButtonEnabled.observeForever { }
         viewModel.isButtonEnabled.value shouldBe false
     }
@@ -89,7 +96,7 @@ class ContactDiaryEditPersonsViewModelTest {
     @Test
     fun testLocations() {
         every { contactDiaryRepository.people } returns MutableStateFlow(personList)
-        viewModel = ContactDiaryEditPersonsViewModel(contactDiaryRepository, TestDispatcherProvider())
+        viewModel = createInstance()
         viewModel.personsLiveData.observeForever { }
         viewModel.personsLiveData.value shouldBe personList
     }
-- 
GitLab