pull/1238/merge
Jaehwa Noh 2 days ago committed by GitHub
commit 7fbb2787d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -24,6 +24,7 @@ import com.google.samples.apps.nowinandroid.core.navigation.NavigationState
import com.google.samples.apps.nowinandroid.core.navigation.Navigator
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey
@ -51,16 +52,19 @@ import kotlin.test.assertEquals
@HiltAndroidTest
class NiaAppStateTest {
@get:Rule
@get:Rule(0)
val composeTestRule = createComposeRule()
@get:Rule(1)
val mainDispatcherRule = MainDispatcherRule()
// Create the test dependencies.
private val networkMonitor = TestNetworkMonitor()
private val timeZoneMonitor = TestTimeZoneMonitor()
private val userNewsResourceRepository =
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository())
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository(), mainDispatcherRule.testDispatcher)
// Subject under test.
private lateinit var state: NiaAppState

@ -18,11 +18,15 @@ package com.google.samples.apps.nowinandroid.core.data.repository
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ -33,8 +37,8 @@ import javax.inject.Inject
class CompositeUserNewsResourceRepository @Inject constructor(
val newsRepository: NewsRepository,
val userDataRepository: UserDataRepository,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) : UserNewsResourceRepository {
/**
* Returns available news resources (joined with user data) matching the given query.
*/
@ -44,7 +48,7 @@ class CompositeUserNewsResourceRepository @Inject constructor(
newsRepository.getNewsResources(query)
.combine(userDataRepository.userData) { newsResources, userData ->
newsResources.mapToUserNewsResources(userData)
}
}.flowOn(defaultDispatcher)
/**
* Returns available news resources (joined with user data) for the followed topics.

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.data.repository
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
@ -25,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -42,6 +45,7 @@ internal class DefaultSearchContentsRepository @Inject constructor(
private val topicDao: TopicDao,
private val topicFtsDao: TopicFtsDao,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) : SearchContentsRepository {
override suspend fun populateFtsData() {
@ -75,11 +79,13 @@ internal class DefaultSearchContentsRepository @Inject constructor(
.distinctUntilChanged()
.flatMapLatest(topicDao::getTopicEntities)
return combine(newsResourcesFlow, topicsFlow) { newsResources, topics ->
SearchResult(
topics = topics.map { it.asExternalModel() },
newsResources = newsResources.map { it.asExternalModel() },
)
}
trace("DefaultSearchContentsRepository.searchContents") {
SearchResult(
topics = topics.map { it.asExternalModel() },
newsResources = newsResources.map { it.asExternalModel() },
)
}
}.flowOn(defaultDispatcher)
}
override fun getSearchContentsCount(): Flow<Int> =
@ -87,6 +93,8 @@ internal class DefaultSearchContentsRepository @Inject constructor(
newsResourceFtsDao.getCount(),
topicFtsDao.getCount(),
) { newsResourceCount, topicsCount ->
newsResourceCount + topicsCount
}
trace("DefaultSearchContentsRepository.getSearchContentsCount") {
newsResourceCount + topicsCount
}
}.flowOn(defaultDispatcher)
}

@ -24,20 +24,26 @@ import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResourc
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class CompositeUserNewsResourceRepositoryTest {
@get:Rule
val dispatcherRule = MainDispatcherRule()
private val newsRepository = TestNewsRepository()
private val userDataRepository = TestUserDataRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
newsRepository = newsRepository,
userDataRepository = userDataRepository,
defaultDispatcher = dispatcherRule.testDispatcher,
)
@Test

@ -16,13 +16,18 @@
package com.google.samples.apps.nowinandroid.core.domain
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NONE
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
/**
@ -31,6 +36,7 @@ import javax.inject.Inject
class GetFollowableTopicsUseCase @Inject constructor(
private val topicsRepository: TopicsRepository,
private val userDataRepository: UserDataRepository,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) {
/**
* Returns a list of topics with their associated followed state.
@ -41,18 +47,20 @@ class GetFollowableTopicsUseCase @Inject constructor(
userDataRepository.userData,
topicsRepository.getTopics(),
) { userData, topics ->
val followedTopics = topics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in userData.followedTopics,
)
trace("GetFollowableTopicsUseCase.invoke") {
val followedTopics = topics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in userData.followedTopics,
)
}
when (sortBy) {
NAME -> followedTopics.sortedBy { it.topic.name }
else -> followedTopics
}
when (sortBy) {
NAME -> followedTopics.sortedBy { it.topic.name }
else -> followedTopics
}
}
}.flowOn(defaultDispatcher)
}
enum class TopicSortField {

@ -23,8 +23,12 @@ import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserSearchResult
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
/**
@ -33,16 +37,20 @@ import javax.inject.Inject
class GetSearchContentsUseCase @Inject constructor(
private val searchContentsRepository: SearchContentsRepository,
private val userDataRepository: UserDataRepository,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) {
operator fun invoke(
searchQuery: String,
): Flow<UserSearchResult> =
searchContentsRepository.searchContents(searchQuery)
.mapToUserSearchResult(userDataRepository.userData)
.mapToUserSearchResult(userDataRepository.userData, defaultDispatcher)
}
private fun Flow<SearchResult>.mapToUserSearchResult(userDataStream: Flow<UserData>): Flow<UserSearchResult> =
private fun Flow<SearchResult>.mapToUserSearchResult(
userDataStream: Flow<UserData>,
defaultDispatcher: CoroutineDispatcher,
): Flow<UserSearchResult> =
combine(userDataStream) { searchResult, userData ->
UserSearchResult(
topics = searchResult.topics.map { topic ->
@ -58,4 +66,4 @@ private fun Flow<SearchResult>.mapToUserSearchResult(userDataStream: Flow<UserDa
)
},
)
}
}.flowOn(defaultDispatcher)

@ -39,6 +39,7 @@ class GetFollowableTopicsUseCaseTest {
val useCase = GetFollowableTopicsUseCase(
topicsRepository,
userDataRepository,
mainDispatcherRule.testDispatcher,
)
@Test

@ -30,7 +30,11 @@ import org.junit.runner.Description
* for the duration of the test.
*/
class MainDispatcherRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
/**
* Expose testDispatcher to share the scheduler to the test.
* See more in [Documentation](https://developer.android.com/kotlin/coroutines/test#injecting-test-dispatchers)
*/
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) = Dispatchers.setMain(testDispatcher)

@ -49,6 +49,7 @@ class BookmarksViewModelTest {
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
newsRepository = newsRepository,
userDataRepository = userDataRepository,
dispatcherRule.testDispatcher,
)
private lateinit var viewModel: BookmarksViewModel

@ -65,11 +65,13 @@ class ForYouViewModelTest {
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
newsRepository = newsRepository,
userDataRepository = userDataRepository,
defaultDispatcher = mainDispatcherRule.testDispatcher,
)
private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase(
topicsRepository = topicsRepository,
userDataRepository = userDataRepository,
defaultDispatcher = mainDispatcherRule.testDispatcher,
)
private val savedStateHandle = SavedStateHandle()

@ -61,6 +61,7 @@ class InterestsViewModelTest {
private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase(
topicsRepository = topicsRepository,
userDataRepository = userDataRepository,
defaultDispatcher = mainDispatcherRule.testDispatcher,
)
private lateinit var viewModel: InterestsViewModel

@ -57,6 +57,7 @@ class SearchViewModelTest {
private val getSearchContentsUseCase = GetSearchContentsUseCase(
searchContentsRepository = searchContentsRepository,
userDataRepository = userDataRepository,
dispatcherRule.testDispatcher,
)
private val recentSearchRepository = TestRecentSearchRepository()
private val getRecentQueryUseCase = GetRecentSearchQueriesUseCase(recentSearchRepository)

@ -52,6 +52,7 @@ class TopicViewModelTest {
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
newsRepository = newsRepository,
userDataRepository = userDataRepository,
defaultDispatcher = dispatcherRule.testDispatcher,
)
private lateinit var viewModel: TopicViewModel

Loading…
Cancel
Save