diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt index 9f43d3441..9f556438a 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt @@ -36,6 +36,8 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -45,6 +47,8 @@ import kotlin.test.assertEquals class OfflineFirstNewsRepositoryTest { + private val testScope = TestScope(UnconfinedTestDispatcher()) + private lateinit var subject: OfflineFirstNewsRepository private lateinit var newsResourceDao: TestNewsResourceDao @@ -65,7 +69,7 @@ class OfflineFirstNewsRepositoryTest { network = TestNiaNetworkDataSource() synchronizer = TestSynchronizer( NiaPreferencesDataSource( - tmpFolder.testUserPreferencesDataStore(), + tmpFolder.testUserPreferencesDataStore(testScope), ), ) @@ -78,7 +82,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_news_resources_stream_is_backed_by_news_resource_dao() = - runTest { + testScope.runTest { assertEquals( newsResourceDao.getNewsResources() .first() @@ -90,7 +94,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_news_resources_for_topic_is_backed_by_news_resource_dao() = - runTest { + testScope.runTest { assertEquals( expected = newsResourceDao.getNewsResources( filterTopicIds = filteredInterestsIds, @@ -119,7 +123,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_sync_pulls_from_network() = - runTest { + testScope.runTest { subject.syncWith(synchronizer) val newsResourcesFromNetwork = network.getNewsResources() @@ -144,7 +148,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_sync_deletes_items_marked_deleted_on_network() = - runTest { + testScope.runTest { val newsResourcesFromNetwork = network.getNewsResources() .map(NetworkNewsResource::asEntity) .map(NewsResourceEntity::asExternalModel) @@ -185,7 +189,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_incremental_sync_pulls_from_network() = - runTest { + testScope.runTest { // Set news version to 7 synchronizer.updateChangeListVersions { copy(newsResourceVersion = 7) @@ -224,7 +228,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_sync_saves_shell_topic_entities() = - runTest { + testScope.runTest { subject.syncWith(synchronizer) assertEquals( @@ -239,7 +243,7 @@ class OfflineFirstNewsRepositoryTest { @Test fun offlineFirstNewsRepository_sync_saves_topic_cross_references() = - runTest { + testScope.runTest { subject.syncWith(synchronizer) assertEquals( diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepositoryTest.kt index ca9941b8a..3bd314eae 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepositoryTest.kt @@ -29,6 +29,8 @@ import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferen import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -38,6 +40,8 @@ import kotlin.test.assertEquals class OfflineFirstTopicsRepositoryTest { + private val testScope = TestScope(UnconfinedTestDispatcher()) + private lateinit var subject: OfflineFirstTopicsRepository private lateinit var topicDao: TopicDao @@ -56,7 +60,7 @@ class OfflineFirstTopicsRepositoryTest { topicDao = TestTopicDao() network = TestNiaNetworkDataSource() niaPreferences = NiaPreferencesDataSource( - tmpFolder.testUserPreferencesDataStore(), + tmpFolder.testUserPreferencesDataStore(testScope), ) synchronizer = TestSynchronizer(niaPreferences) @@ -68,7 +72,7 @@ class OfflineFirstTopicsRepositoryTest { @Test fun offlineFirstTopicsRepository_topics_stream_is_backed_by_topics_dao() = - runTest { + testScope.runTest { assertEquals( topicDao.getTopicEntities() .first() @@ -80,7 +84,7 @@ class OfflineFirstTopicsRepositoryTest { @Test fun offlineFirstTopicsRepository_sync_pulls_from_network() = - runTest { + testScope.runTest { subject.syncWith(synchronizer) val networkTopics = network.getTopics() @@ -103,7 +107,7 @@ class OfflineFirstTopicsRepositoryTest { @Test fun offlineFirstTopicsRepository_incremental_sync_pulls_from_network() = - runTest { + testScope.runTest { // Set topics version to 10 synchronizer.updateChangeListVersions { copy(topicVersion = 10) @@ -133,7 +137,7 @@ class OfflineFirstTopicsRepositoryTest { @Test fun offlineFirstTopicsRepository_sync_deletes_items_marked_deleted_on_network() = - runTest { + testScope.runTest { val networkTopics = network.getTopics() .map(NetworkTopic::asEntity) .map(TopicEntity::asExternalModel) diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt index 61569d650..daf1a6564 100644 --- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt +++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt @@ -24,6 +24,8 @@ import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand import com.google.samples.apps.nowinandroid.core.model.data.UserData import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -34,6 +36,9 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class OfflineFirstUserDataRepositoryTest { + + private val testScope = TestScope(UnconfinedTestDispatcher()) + private lateinit var subject: OfflineFirstUserDataRepository private lateinit var niaPreferencesDataSource: NiaPreferencesDataSource @@ -46,7 +51,7 @@ class OfflineFirstUserDataRepositoryTest { @Before fun setup() { niaPreferencesDataSource = NiaPreferencesDataSource( - tmpFolder.testUserPreferencesDataStore(), + tmpFolder.testUserPreferencesDataStore(testScope), ) subject = OfflineFirstUserDataRepository( @@ -57,7 +62,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_default_user_data_is_correct() = - runTest { + testScope.runTest { assertEquals( UserData( bookmarkedNewsResources = emptySet(), @@ -73,7 +78,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() = - runTest { + testScope.runTest { subject.toggleFollowedTopicId(followedTopicId = "0", followed = true) assertEquals( @@ -104,7 +109,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_set_followed_topics_logic_delegates_to_nia_preferences() = - runTest { + testScope.runTest { subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2")) assertEquals( @@ -126,7 +131,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() = - runTest { + testScope.runTest { subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true) assertEquals( @@ -157,7 +162,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_set_theme_brand_delegates_to_nia_preferences() = - runTest { + testScope.runTest { subject.setThemeBrand(ThemeBrand.ANDROID) assertEquals( @@ -177,7 +182,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_set_dynamic_color_delegates_to_nia_preferences() = - runTest { + testScope.runTest { subject.setDynamicColorPreference(true) assertEquals( @@ -197,7 +202,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() = - runTest { + testScope.runTest { subject.setDarkThemeConfig(DarkThemeConfig.DARK) assertEquals( @@ -217,7 +222,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun whenUserCompletesOnboarding_thenRemovesAllInterests_shouldHideOnboardingIsFalse() = - runTest { + testScope.runTest { subject.setFollowedTopicIds(setOf("1")) subject.setShouldHideOnboarding(true) assertTrue(subject.userData.first().shouldHideOnboarding) diff --git a/core/datastore-test/build.gradle.kts b/core/datastore-test/build.gradle.kts index 40b287b7b..d8223c3f3 100644 --- a/core/datastore-test/build.gradle.kts +++ b/core/datastore-test/build.gradle.kts @@ -24,6 +24,7 @@ android { dependencies { api(project(":core:datastore")) + implementation(project(":core:common")) implementation(project(":core:testing")) api(libs.androidx.dataStore.core) diff --git a/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt b/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt index b29728cf1..0e83bb296 100644 --- a/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt +++ b/core/datastore-test/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt @@ -21,10 +21,15 @@ import androidx.datastore.core.DataStoreFactory import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer import com.google.samples.apps.nowinandroid.core.datastore.di.DataStoreModule +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent import dagger.hilt.testing.TestInstallIn +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import org.junit.rules.TemporaryFolder import javax.inject.Singleton @@ -38,16 +43,23 @@ object TestDataStoreModule { @Provides @Singleton fun providesUserPreferencesDataStore( + @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, userPreferencesSerializer: UserPreferencesSerializer, tmpFolder: TemporaryFolder, ): DataStore = - tmpFolder.testUserPreferencesDataStore(userPreferencesSerializer) + tmpFolder.testUserPreferencesDataStore( + // TODO: Provide an application-wide CoroutineScope in the DI graph + coroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher), + userPreferencesSerializer = userPreferencesSerializer + ) } fun TemporaryFolder.testUserPreferencesDataStore( + coroutineScope: CoroutineScope, userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(), ) = DataStoreFactory.create( serializer = userPreferencesSerializer, + scope = coroutineScope ) { newFile("user_preferences_test.pb") } diff --git a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt index 0d047d310..b865aa431 100644 --- a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt +++ b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSourceTest.kt @@ -18,6 +18,8 @@ package com.google.samples.apps.nowinandroid.core.datastore import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -27,6 +29,9 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class NiaPreferencesDataSourceTest { + + private val testScope = TestScope(UnconfinedTestDispatcher()) + private lateinit var subject: NiaPreferencesDataSource @get:Rule @@ -35,54 +40,56 @@ class NiaPreferencesDataSourceTest { @Before fun setup() { subject = NiaPreferencesDataSource( - tmpFolder.testUserPreferencesDataStore(), + tmpFolder.testUserPreferencesDataStore(testScope), ) } @Test - fun shouldHideOnboardingIsFalseByDefault() = runTest { + fun shouldHideOnboardingIsFalseByDefault() = testScope.runTest { assertFalse(subject.userData.first().shouldHideOnboarding) } @Test - fun userShouldHideOnboardingIsTrueWhenSet() = runTest { + fun userShouldHideOnboardingIsTrueWhenSet() = testScope.runTest { subject.setShouldHideOnboarding(true) assertTrue(subject.userData.first().shouldHideOnboarding) } @Test - fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() = runTest { - // Given: user completes onboarding by selecting a single topic. - subject.toggleFollowedTopicId("1", true) - subject.setShouldHideOnboarding(true) + fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() = + testScope.runTest { + // Given: user completes onboarding by selecting a single topic. + subject.toggleFollowedTopicId("1", true) + subject.setShouldHideOnboarding(true) - // When: they unfollow that topic. - subject.toggleFollowedTopicId("1", false) + // When: they unfollow that topic. + subject.toggleFollowedTopicId("1", false) - // Then: onboarding should be shown again - assertFalse(subject.userData.first().shouldHideOnboarding) - } + // Then: onboarding should be shown again + assertFalse(subject.userData.first().shouldHideOnboarding) + } @Test - fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() = runTest { - // Given: user completes onboarding by selecting several topics. - subject.setFollowedTopicIds(setOf("1", "2")) - subject.setShouldHideOnboarding(true) + fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() = + testScope.runTest { + // Given: user completes onboarding by selecting several topics. + subject.setFollowedTopicIds(setOf("1", "2")) + subject.setShouldHideOnboarding(true) - // When: they unfollow those topics. - subject.setFollowedTopicIds(emptySet()) + // When: they unfollow those topics. + subject.setFollowedTopicIds(emptySet()) - // Then: onboarding should be shown again - assertFalse(subject.userData.first().shouldHideOnboarding) - } + // Then: onboarding should be shown again + assertFalse(subject.userData.first().shouldHideOnboarding) + } @Test - fun shouldUseDynamicColorFalseByDefault() = runTest { + fun shouldUseDynamicColorFalseByDefault() = testScope.runTest { assertFalse(subject.userData.first().useDynamicColor) } @Test - fun userShouldUseDynamicColorIsTrueWhenSet() = runTest { + fun userShouldUseDynamicColorIsTrueWhenSet() = testScope.runTest { subject.setDynamicColorPreference(true) assertTrue(subject.userData.first().useDynamicColor) }