Provide a CoroutineScope to fake DataStores

pull/607/head
Manuel Vivo 1 year ago
parent c677db2344
commit 91633af376

@ -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.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -45,6 +47,8 @@ import kotlin.test.assertEquals
class OfflineFirstNewsRepositoryTest { class OfflineFirstNewsRepositoryTest {
private val testScope = TestScope(UnconfinedTestDispatcher())
private lateinit var subject: OfflineFirstNewsRepository private lateinit var subject: OfflineFirstNewsRepository
private lateinit var newsResourceDao: TestNewsResourceDao private lateinit var newsResourceDao: TestNewsResourceDao
@ -65,7 +69,7 @@ class OfflineFirstNewsRepositoryTest {
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()
synchronizer = TestSynchronizer( synchronizer = TestSynchronizer(
NiaPreferencesDataSource( NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(), tmpFolder.testUserPreferencesDataStore(testScope),
), ),
) )
@ -78,7 +82,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_news_resources_stream_is_backed_by_news_resource_dao() = fun offlineFirstNewsRepository_news_resources_stream_is_backed_by_news_resource_dao() =
runTest { testScope.runTest {
assertEquals( assertEquals(
newsResourceDao.getNewsResources() newsResourceDao.getNewsResources()
.first() .first()
@ -90,7 +94,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_news_resources_for_topic_is_backed_by_news_resource_dao() = fun offlineFirstNewsRepository_news_resources_for_topic_is_backed_by_news_resource_dao() =
runTest { testScope.runTest {
assertEquals( assertEquals(
expected = newsResourceDao.getNewsResources( expected = newsResourceDao.getNewsResources(
filterTopicIds = filteredInterestsIds, filterTopicIds = filteredInterestsIds,
@ -119,7 +123,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_sync_pulls_from_network() = fun offlineFirstNewsRepository_sync_pulls_from_network() =
runTest { testScope.runTest {
subject.syncWith(synchronizer) subject.syncWith(synchronizer)
val newsResourcesFromNetwork = network.getNewsResources() val newsResourcesFromNetwork = network.getNewsResources()
@ -144,7 +148,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_sync_deletes_items_marked_deleted_on_network() = fun offlineFirstNewsRepository_sync_deletes_items_marked_deleted_on_network() =
runTest { testScope.runTest {
val newsResourcesFromNetwork = network.getNewsResources() val newsResourcesFromNetwork = network.getNewsResources()
.map(NetworkNewsResource::asEntity) .map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel) .map(NewsResourceEntity::asExternalModel)
@ -185,7 +189,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_incremental_sync_pulls_from_network() = fun offlineFirstNewsRepository_incremental_sync_pulls_from_network() =
runTest { testScope.runTest {
// Set news version to 7 // Set news version to 7
synchronizer.updateChangeListVersions { synchronizer.updateChangeListVersions {
copy(newsResourceVersion = 7) copy(newsResourceVersion = 7)
@ -224,7 +228,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_sync_saves_shell_topic_entities() = fun offlineFirstNewsRepository_sync_saves_shell_topic_entities() =
runTest { testScope.runTest {
subject.syncWith(synchronizer) subject.syncWith(synchronizer)
assertEquals( assertEquals(
@ -239,7 +243,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_sync_saves_topic_cross_references() = fun offlineFirstNewsRepository_sync_saves_topic_cross_references() =
runTest { testScope.runTest {
subject.syncWith(synchronizer) subject.syncWith(synchronizer)
assertEquals( assertEquals(

@ -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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -38,6 +40,8 @@ import kotlin.test.assertEquals
class OfflineFirstTopicsRepositoryTest { class OfflineFirstTopicsRepositoryTest {
private val testScope = TestScope(UnconfinedTestDispatcher())
private lateinit var subject: OfflineFirstTopicsRepository private lateinit var subject: OfflineFirstTopicsRepository
private lateinit var topicDao: TopicDao private lateinit var topicDao: TopicDao
@ -56,7 +60,7 @@ class OfflineFirstTopicsRepositoryTest {
topicDao = TestTopicDao() topicDao = TestTopicDao()
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()
niaPreferences = NiaPreferencesDataSource( niaPreferences = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(), tmpFolder.testUserPreferencesDataStore(testScope),
) )
synchronizer = TestSynchronizer(niaPreferences) synchronizer = TestSynchronizer(niaPreferences)
@ -68,7 +72,7 @@ class OfflineFirstTopicsRepositoryTest {
@Test @Test
fun offlineFirstTopicsRepository_topics_stream_is_backed_by_topics_dao() = fun offlineFirstTopicsRepository_topics_stream_is_backed_by_topics_dao() =
runTest { testScope.runTest {
assertEquals( assertEquals(
topicDao.getTopicEntities() topicDao.getTopicEntities()
.first() .first()
@ -80,7 +84,7 @@ class OfflineFirstTopicsRepositoryTest {
@Test @Test
fun offlineFirstTopicsRepository_sync_pulls_from_network() = fun offlineFirstTopicsRepository_sync_pulls_from_network() =
runTest { testScope.runTest {
subject.syncWith(synchronizer) subject.syncWith(synchronizer)
val networkTopics = network.getTopics() val networkTopics = network.getTopics()
@ -103,7 +107,7 @@ class OfflineFirstTopicsRepositoryTest {
@Test @Test
fun offlineFirstTopicsRepository_incremental_sync_pulls_from_network() = fun offlineFirstTopicsRepository_incremental_sync_pulls_from_network() =
runTest { testScope.runTest {
// Set topics version to 10 // Set topics version to 10
synchronizer.updateChangeListVersions { synchronizer.updateChangeListVersions {
copy(topicVersion = 10) copy(topicVersion = 10)
@ -133,7 +137,7 @@ class OfflineFirstTopicsRepositoryTest {
@Test @Test
fun offlineFirstTopicsRepository_sync_deletes_items_marked_deleted_on_network() = fun offlineFirstTopicsRepository_sync_deletes_items_marked_deleted_on_network() =
runTest { testScope.runTest {
val networkTopics = network.getTopics() val networkTopics = network.getTopics()
.map(NetworkTopic::asEntity) .map(NetworkTopic::asEntity)
.map(TopicEntity::asExternalModel) .map(TopicEntity::asExternalModel)

@ -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 com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -34,6 +36,9 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class OfflineFirstUserDataRepositoryTest { class OfflineFirstUserDataRepositoryTest {
private val testScope = TestScope(UnconfinedTestDispatcher())
private lateinit var subject: OfflineFirstUserDataRepository private lateinit var subject: OfflineFirstUserDataRepository
private lateinit var niaPreferencesDataSource: NiaPreferencesDataSource private lateinit var niaPreferencesDataSource: NiaPreferencesDataSource
@ -46,7 +51,7 @@ class OfflineFirstUserDataRepositoryTest {
@Before @Before
fun setup() { fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource( niaPreferencesDataSource = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(), tmpFolder.testUserPreferencesDataStore(testScope),
) )
subject = OfflineFirstUserDataRepository( subject = OfflineFirstUserDataRepository(
@ -57,7 +62,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_default_user_data_is_correct() = fun offlineFirstUserDataRepository_default_user_data_is_correct() =
runTest { testScope.runTest {
assertEquals( assertEquals(
UserData( UserData(
bookmarkedNewsResources = emptySet(), bookmarkedNewsResources = emptySet(),
@ -73,7 +78,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
runTest { testScope.runTest {
subject.toggleFollowedTopicId(followedTopicId = "0", followed = true) subject.toggleFollowedTopicId(followedTopicId = "0", followed = true)
assertEquals( assertEquals(
@ -104,7 +109,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_set_followed_topics_logic_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_set_followed_topics_logic_delegates_to_nia_preferences() =
runTest { testScope.runTest {
subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2")) subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2"))
assertEquals( assertEquals(
@ -126,7 +131,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() =
runTest { testScope.runTest {
subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true) subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true)
assertEquals( assertEquals(
@ -157,7 +162,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_set_theme_brand_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_set_theme_brand_delegates_to_nia_preferences() =
runTest { testScope.runTest {
subject.setThemeBrand(ThemeBrand.ANDROID) subject.setThemeBrand(ThemeBrand.ANDROID)
assertEquals( assertEquals(
@ -177,7 +182,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_set_dynamic_color_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_set_dynamic_color_delegates_to_nia_preferences() =
runTest { testScope.runTest {
subject.setDynamicColorPreference(true) subject.setDynamicColorPreference(true)
assertEquals( assertEquals(
@ -197,7 +202,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() = fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() =
runTest { testScope.runTest {
subject.setDarkThemeConfig(DarkThemeConfig.DARK) subject.setDarkThemeConfig(DarkThemeConfig.DARK)
assertEquals( assertEquals(
@ -217,7 +222,7 @@ class OfflineFirstUserDataRepositoryTest {
@Test @Test
fun whenUserCompletesOnboarding_thenRemovesAllInterests_shouldHideOnboardingIsFalse() = fun whenUserCompletesOnboarding_thenRemovesAllInterests_shouldHideOnboardingIsFalse() =
runTest { testScope.runTest {
subject.setFollowedTopicIds(setOf("1")) subject.setFollowedTopicIds(setOf("1"))
subject.setShouldHideOnboarding(true) subject.setShouldHideOnboarding(true)
assertTrue(subject.userData.first().shouldHideOnboarding) assertTrue(subject.userData.first().shouldHideOnboarding)

@ -24,6 +24,7 @@ android {
dependencies { dependencies {
api(project(":core:datastore")) api(project(":core:datastore"))
implementation(project(":core:common"))
implementation(project(":core:testing")) implementation(project(":core:testing"))
api(libs.androidx.dataStore.core) api(libs.androidx.dataStore.core)

@ -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.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer 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.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.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn import dagger.hilt.testing.TestInstallIn
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import javax.inject.Singleton import javax.inject.Singleton
@ -38,16 +43,23 @@ object TestDataStoreModule {
@Provides @Provides
@Singleton @Singleton
fun providesUserPreferencesDataStore( fun providesUserPreferencesDataStore(
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
userPreferencesSerializer: UserPreferencesSerializer, userPreferencesSerializer: UserPreferencesSerializer,
tmpFolder: TemporaryFolder, tmpFolder: TemporaryFolder,
): DataStore<UserPreferences> = ): DataStore<UserPreferences> =
tmpFolder.testUserPreferencesDataStore(userPreferencesSerializer) tmpFolder.testUserPreferencesDataStore(
// TODO: Provide an application-wide CoroutineScope in the DI graph
coroutineScope = CoroutineScope(SupervisorJob() + ioDispatcher),
userPreferencesSerializer = userPreferencesSerializer
)
} }
fun TemporaryFolder.testUserPreferencesDataStore( fun TemporaryFolder.testUserPreferencesDataStore(
coroutineScope: CoroutineScope,
userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(), userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(),
) = DataStoreFactory.create( ) = DataStoreFactory.create(
serializer = userPreferencesSerializer, serializer = userPreferencesSerializer,
scope = coroutineScope
) { ) {
newFile("user_preferences_test.pb") newFile("user_preferences_test.pb")
} }

@ -18,6 +18,8 @@ package com.google.samples.apps.nowinandroid.core.datastore
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -27,6 +29,9 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class NiaPreferencesDataSourceTest { class NiaPreferencesDataSourceTest {
private val testScope = TestScope(UnconfinedTestDispatcher())
private lateinit var subject: NiaPreferencesDataSource private lateinit var subject: NiaPreferencesDataSource
@get:Rule @get:Rule
@ -35,54 +40,56 @@ class NiaPreferencesDataSourceTest {
@Before @Before
fun setup() { fun setup() {
subject = NiaPreferencesDataSource( subject = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(), tmpFolder.testUserPreferencesDataStore(testScope),
) )
} }
@Test @Test
fun shouldHideOnboardingIsFalseByDefault() = runTest { fun shouldHideOnboardingIsFalseByDefault() = testScope.runTest {
assertFalse(subject.userData.first().shouldHideOnboarding) assertFalse(subject.userData.first().shouldHideOnboarding)
} }
@Test @Test
fun userShouldHideOnboardingIsTrueWhenSet() = runTest { fun userShouldHideOnboardingIsTrueWhenSet() = testScope.runTest {
subject.setShouldHideOnboarding(true) subject.setShouldHideOnboarding(true)
assertTrue(subject.userData.first().shouldHideOnboarding) assertTrue(subject.userData.first().shouldHideOnboarding)
} }
@Test @Test
fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() = runTest { fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() =
// Given: user completes onboarding by selecting a single topic. testScope.runTest {
subject.toggleFollowedTopicId("1", true) // Given: user completes onboarding by selecting a single topic.
subject.setShouldHideOnboarding(true) subject.toggleFollowedTopicId("1", true)
subject.setShouldHideOnboarding(true)
// When: they unfollow that topic. // When: they unfollow that topic.
subject.toggleFollowedTopicId("1", false) subject.toggleFollowedTopicId("1", false)
// Then: onboarding should be shown again // Then: onboarding should be shown again
assertFalse(subject.userData.first().shouldHideOnboarding) assertFalse(subject.userData.first().shouldHideOnboarding)
} }
@Test @Test
fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() = runTest { fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() =
// Given: user completes onboarding by selecting several topics. testScope.runTest {
subject.setFollowedTopicIds(setOf("1", "2")) // Given: user completes onboarding by selecting several topics.
subject.setShouldHideOnboarding(true) subject.setFollowedTopicIds(setOf("1", "2"))
subject.setShouldHideOnboarding(true)
// When: they unfollow those topics. // When: they unfollow those topics.
subject.setFollowedTopicIds(emptySet()) subject.setFollowedTopicIds(emptySet())
// Then: onboarding should be shown again // Then: onboarding should be shown again
assertFalse(subject.userData.first().shouldHideOnboarding) assertFalse(subject.userData.first().shouldHideOnboarding)
} }
@Test @Test
fun shouldUseDynamicColorFalseByDefault() = runTest { fun shouldUseDynamicColorFalseByDefault() = testScope.runTest {
assertFalse(subject.userData.first().useDynamicColor) assertFalse(subject.userData.first().useDynamicColor)
} }
@Test @Test
fun userShouldUseDynamicColorIsTrueWhenSet() = runTest { fun userShouldUseDynamicColorIsTrueWhenSet() = testScope.runTest {
subject.setDynamicColorPreference(true) subject.setDynamicColorPreference(true)
assertTrue(subject.userData.first().useDynamicColor) assertTrue(subject.userData.first().useDynamicColor)
} }

Loading…
Cancel
Save