pull/1238/merge
Jaehwa Noh 9 months ago committed by GitHub
commit 35b26bdce7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -31,6 +31,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepo
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor 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.core.testing.util.TestTimeZoneMonitor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -58,7 +59,11 @@ class NiaAppStateTest {
private val timeZoneMonitor = TestTimeZoneMonitor() private val timeZoneMonitor = TestTimeZoneMonitor()
private val userNewsResourceRepository = private val userNewsResourceRepository =
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository()) CompositeUserNewsResourceRepository(
newsRepository = TestNewsRepository(),
userDataRepository = TestUserDataRepository(),
defaultDispatcher = Dispatchers.Default,
)
// Subject under test. // Subject under test.
private lateinit var state: NiaAppState 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.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources 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.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
@ -33,6 +37,7 @@ import javax.inject.Inject
class CompositeUserNewsResourceRepository @Inject constructor( class CompositeUserNewsResourceRepository @Inject constructor(
val newsRepository: NewsRepository, val newsRepository: NewsRepository,
val userDataRepository: UserDataRepository, val userDataRepository: UserDataRepository,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) : UserNewsResourceRepository { ) : UserNewsResourceRepository {
/** /**
@ -44,7 +49,7 @@ class CompositeUserNewsResourceRepository @Inject constructor(
newsRepository.getNewsResources(query) newsRepository.getNewsResources(query)
.combine(userDataRepository.userData) { newsResources, userData -> .combine(userDataRepository.userData) { newsResources, userData ->
newsResources.mapToUserNewsResources(userData) newsResources.mapToUserNewsResources(userData)
} }.flowOn(defaultDispatcher)
/** /**
* Returns available news resources (joined with user data) for the followed topics. * 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 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.NewsResourceDao
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao 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.database.model.asFtsEntity
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult 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.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -42,6 +45,7 @@ internal class DefaultSearchContentsRepository @Inject constructor(
private val topicDao: TopicDao, private val topicDao: TopicDao,
private val topicFtsDao: TopicFtsDao, private val topicFtsDao: TopicFtsDao,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) : SearchContentsRepository { ) : SearchContentsRepository {
override suspend fun populateFtsData() { override suspend fun populateFtsData() {
@ -75,11 +79,13 @@ internal class DefaultSearchContentsRepository @Inject constructor(
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest(topicDao::getTopicEntities) .flatMapLatest(topicDao::getTopicEntities)
return combine(newsResourcesFlow, topicsFlow) { newsResources, topics -> return combine(newsResourcesFlow, topicsFlow) { newsResources, topics ->
trace("DefaultSearchContentsRepository.searchContents") {
SearchResult( SearchResult(
topics = topics.map { it.asExternalModel() }, topics = topics.map { it.asExternalModel() },
newsResources = newsResources.map { it.asExternalModel() }, newsResources = newsResources.map { it.asExternalModel() },
) )
} }
}.flowOn(defaultDispatcher)
} }
override fun getSearchContentsCount(): Flow<Int> = override fun getSearchContentsCount(): Flow<Int> =
@ -87,6 +93,8 @@ internal class DefaultSearchContentsRepository @Inject constructor(
newsResourceFtsDao.getCount(), newsResourceFtsDao.getCount(),
topicFtsDao.getCount(), topicFtsDao.getCount(),
) { newsResourceCount, topicsCount -> ) { newsResourceCount, topicsCount ->
trace("DefaultSearchContentsRepository.getSearchContentsCount") {
newsResourceCount + topicsCount 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.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository 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.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class CompositeUserNewsResourceRepositoryTest { class CompositeUserNewsResourceRepositoryTest {
@get:Rule
val dispatcherRule = MainDispatcherRule()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private val userDataRepository = TestUserDataRepository() private val userDataRepository = TestUserDataRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository( private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
newsRepository = newsRepository, newsRepository = newsRepository,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
defaultDispatcher = dispatcherRule.testDispatcher,
) )
@Test @Test

@ -16,13 +16,18 @@
package com.google.samples.apps.nowinandroid.core.domain 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.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository 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.NAME
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NONE 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.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.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -31,6 +36,7 @@ import javax.inject.Inject
class GetFollowableTopicsUseCase @Inject constructor( class GetFollowableTopicsUseCase @Inject constructor(
private val topicsRepository: TopicsRepository, private val topicsRepository: TopicsRepository,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) { ) {
/** /**
* Returns a list of topics with their associated followed state. * Returns a list of topics with their associated followed state.
@ -41,6 +47,7 @@ class GetFollowableTopicsUseCase @Inject constructor(
userDataRepository.userData, userDataRepository.userData,
topicsRepository.getTopics(), topicsRepository.getTopics(),
) { userData, topics -> ) { userData, topics ->
trace("GetFollowableTopicsUseCase.invoke") {
val followedTopics = topics val followedTopics = topics
.map { topic -> .map { topic ->
FollowableTopic( FollowableTopic(
@ -53,6 +60,7 @@ class GetFollowableTopicsUseCase @Inject constructor(
else -> followedTopics else -> followedTopics
} }
} }
}.flowOn(defaultDispatcher)
} }
enum class TopicSortField { enum class TopicSortField {

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.domain package com.google.samples.apps.nowinandroid.core.domain
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@ -23,8 +24,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.UserData
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource 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.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.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -33,17 +38,22 @@ import javax.inject.Inject
class GetSearchContentsUseCase @Inject constructor( class GetSearchContentsUseCase @Inject constructor(
private val searchContentsRepository: SearchContentsRepository, private val searchContentsRepository: SearchContentsRepository,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
@Dispatcher(Default) private val defaultDispatcher: CoroutineDispatcher,
) { ) {
operator fun invoke( operator fun invoke(
searchQuery: String, searchQuery: String,
): Flow<UserSearchResult> = ): Flow<UserSearchResult> =
searchContentsRepository.searchContents(searchQuery) 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 -> combine(userDataStream) { searchResult, userData ->
trace("Flow<SearchResult>.mapToUserSearchResult") {
UserSearchResult( UserSearchResult(
topics = searchResult.topics.map { topic -> topics = searchResult.topics.map { topic ->
FollowableTopic( FollowableTopic(
@ -59,3 +69,4 @@ private fun Flow<SearchResult>.mapToUserSearchResult(userDataStream: Flow<UserDa
}, },
) )
} }
}.flowOn(defaultDispatcher)

@ -37,8 +37,9 @@ class GetFollowableTopicsUseCaseTest {
private val userDataRepository = TestUserDataRepository() private val userDataRepository = TestUserDataRepository()
val useCase = GetFollowableTopicsUseCase( val useCase = GetFollowableTopicsUseCase(
topicsRepository, topicsRepository = topicsRepository,
userDataRepository, userDataRepository = userDataRepository,
defaultDispatcher = mainDispatcherRule.testDispatcher,
) )
@Test @Test

@ -30,7 +30,11 @@ import org.junit.runner.Description
* for the duration of the test. * for the duration of the test.
*/ */
class MainDispatcherRule( 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() { ) : TestWatcher() {
override fun starting(description: Description) = Dispatchers.setMain(testDispatcher) override fun starting(description: Description) = Dispatchers.setMain(testDispatcher)

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

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

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

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

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

Loading…
Cancel
Save