From 65949f1b70eaa6970e45cd196c362923c982073e Mon Sep 17 00:00:00 2001 From: takahirom Date: Fri, 29 Dec 2017 23:25:27 +0900 Subject: [PATCH] Refactor around search --- .../data/repository/SessionDataRepository.kt | 14 ++- .../data/repository/SessionRepository.kt | 1 + .../common/mapper/ResultMapperExt.kt | 7 +- .../presentation/search/SearchFragment.kt | 4 +- .../search/SearchTopicsFragment.kt | 2 +- .../presentation/search/SearchViewModel.kt | 19 +--- .../confsched2018/DummyDataCreator.kt | 104 ++++++++++++++---- .../repository/SessionsDataRepositoryTest.kt | 63 +++++------ .../detail/SessionDetailViewModelTest.kt | 6 +- .../search/SearchViewModelTest.kt | 82 ++++++++++++++ common-jvm/src/main/kotlin/JvmDate.kt | 4 + common/src/main/kotlin/SearchResult.kt | 6 + 12 files changed, 226 insertions(+), 86 deletions(-) create mode 100644 app/src/test/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModelTest.kt create mode 100644 common/src/main/kotlin/SearchResult.kt diff --git a/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionDataRepository.kt b/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionDataRepository.kt index 8b3d8008..cec1fba3 100644 --- a/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionDataRepository.kt +++ b/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionDataRepository.kt @@ -12,6 +12,7 @@ import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Single import io.reactivex.rxkotlin.Flowables +import io.reactivex.rxkotlin.Singles import timber.log.Timber import javax.inject.Inject @@ -47,8 +48,8 @@ class SessionDataRepository @Inject constructor( override val speakers: Flowable> = sessionDatabase.getAllSpeaker() .map { speakers -> - speakers.map { speaker -> speaker.toSpeaker() } - } + speakers.map { speaker -> speaker.toSpeaker() } + } override val roomSessions: Flowable>> = sessions.map { sessionList -> sessionList.groupBy { it.room } } @@ -70,6 +71,15 @@ class SessionDataRepository @Inject constructor( .toCompletable() } + override fun search(query: String): Single = Singles.zip( + sessions.map { + it.filter { it.title.contains(query) || it.desc.contains(query) } + }.firstOrError(), + speakers.map { + it.filter { it.name.contains(query) } + }.firstOrError(), + { sessions: List, speakers: List -> SearchResult(sessions, speakers) }) + companion object { const val DEBUG = false } diff --git a/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionRepository.kt b/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionRepository.kt index 1dded07e..5cdaaf07 100644 --- a/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionRepository.kt +++ b/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/SessionRepository.kt @@ -16,5 +16,6 @@ interface SessionRepository { @CheckResult fun refreshSessions(): Completable @CheckResult fun favorite(session: Session): Single + @CheckResult fun search(query: String): Single } diff --git a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/common/mapper/ResultMapperExt.kt b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/common/mapper/ResultMapperExt.kt index efd09141..79da42c5 100644 --- a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/common/mapper/ResultMapperExt.kt +++ b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/common/mapper/ResultMapperExt.kt @@ -25,9 +25,4 @@ fun Observable.toResult(schedulerProvider: SchedulerProvider): Observable } fun Single.toResult(schedulerProvider: SchedulerProvider): Observable> = - compose { item -> - item - .map { Result.success(it) } - .onErrorReturn { e -> Result.failure(e.message ?: "unknown", e) } - .observeOn(schedulerProvider.ui()) - }.toObservable().startWith(Result.inProgress()) + toObservable().toResult(schedulerProvider) diff --git a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchFragment.kt b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchFragment.kt index b0559b38..e01aa3ff 100644 --- a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchFragment.kt +++ b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchFragment.kt @@ -71,10 +71,10 @@ class SearchFragment : Fragment(), Injectable { private fun setupSearch() { setupRecyclerView() - searchViewModel.sessions.observe(this, { result -> + searchViewModel.result.observe(this, { result -> when (result) { is Result.Success -> { - val sessions = result.data + val sessions = result.data.sessions sessionsGroup.updateSessions(sessions, onFavoriteClickListener) binding.sessionsRecycler.scrollToPosition(0) } diff --git a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchTopicsFragment.kt b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchTopicsFragment.kt index 2a1112be..66f26d6f 100644 --- a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchTopicsFragment.kt +++ b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchTopicsFragment.kt @@ -46,7 +46,7 @@ class SearchTopicsFragment : Fragment(), Injectable { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView() - // TODO: searchTopicsViewModel.sessions fetch data here + // TODO: searchTopicsViewModel.result fetch data here lifecycle.addObserver(searchTopicsViewModel) } diff --git a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModel.kt b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModel.kt index 78740ed3..95f9a0e3 100644 --- a/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModel.kt +++ b/app/src/main/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModel.kt @@ -4,8 +4,8 @@ import android.arch.lifecycle.LifecycleObserver import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.ViewModel import io.github.droidkaigi.confsched2018.data.repository.SessionRepository +import io.github.droidkaigi.confsched2018.model.SearchResult import io.github.droidkaigi.confsched2018.model.Session -import io.github.droidkaigi.confsched2018.model.Speaker import io.github.droidkaigi.confsched2018.presentation.Result import io.github.droidkaigi.confsched2018.presentation.common.mapper.toResult import io.github.droidkaigi.confsched2018.util.defaultErrorHandler @@ -20,24 +20,15 @@ class SearchViewModel @Inject constructor( private val repository: SessionRepository, private val schedulerProvider: SchedulerProvider ) : ViewModel(), LifecycleObserver { - val sessions: MutableLiveData>> = MutableLiveData() - val speakers: MutableLiveData>> = MutableLiveData() + val result: MutableLiveData> = MutableLiveData() private val compositeDisposable: CompositeDisposable = CompositeDisposable() fun onQuery(query: String) { - repository.sessions - .map { - it.filter { it.title.contains(query) || it.desc.contains(query) } - } + repository.search(query) .toResult(schedulerProvider) - .subscribe { sessions.value = it } - .addTo(compositeDisposable) - repository.speakers - .map { - it.filter { it.name.contains(query) } + .subscribe { + result.value = it } - .toResult(schedulerProvider) - .subscribe { speakers.value = it } .addTo(compositeDisposable) } diff --git a/app/src/test/java/io/github/droidkaigi/confsched2018/DummyDataCreator.kt b/app/src/test/java/io/github/droidkaigi/confsched2018/DummyDataCreator.kt index 3f86212c..3825e599 100644 --- a/app/src/test/java/io/github/droidkaigi/confsched2018/DummyDataCreator.kt +++ b/app/src/test/java/io/github/droidkaigi/confsched2018/DummyDataCreator.kt @@ -1,32 +1,41 @@ package io.github.droidkaigi.confsched2018 +import io.github.droidkaigi.confsched2018.data.db.entity.* import io.github.droidkaigi.confsched2018.model.* +import org.threeten.bp.LocalDateTime -val DUMMY_SESSION_ID_1 = "test1" -val DUMMY_SESSION_ID_2 = "test2" +const val DUMMY_SESSION_ID1 = "test1" +const val DUMMY_SESSION_ID2 = "test2" +const val DUMMY_SESSION_TITLE1 = "DroidKaigi" +const val DUMMY_SESSION_TITLE2 = "RejectKaigi" fun createDummySessions(): List = - listOf(createDummySession(DUMMY_SESSION_ID_1), createDummySession(DUMMY_SESSION_ID_2)) - -fun createDummySession(sessionId: String): Session = Session( - id = sessionId, - title = "DroidKaigi", - desc = "How to create DroidKaigi app", - startTime = parseDate(10000), - endTime = parseDate(10000), - format = "30分", - room = Room(1, "Hall"), - topic = Topic(2, "Development tool"), - language = "JA", - level = Level(1, "Beginner"), - speakers = listOf( - createSpeaker(), - createSpeaker() - ), - isFavorited = true -) - -private fun createSpeaker(): Speaker { + listOf( + createDummySession(DUMMY_SESSION_ID1, DUMMY_SESSION_TITLE1), + createDummySession(DUMMY_SESSION_ID2, DUMMY_SESSION_TITLE2) + ) + +fun createDummySession(sessionId: String = DUMMY_SESSION_ID1, title: String = DUMMY_SESSION_TITLE1): Session { + return Session( + id = sessionId, + title = title, + desc = "How to create DroidKaigi app", + startTime = parseDate(10000), + endTime = parseDate(10000), + format = "30分", + room = Room(1, "Hall"), + topic = Topic(2, "Development tool"), + language = "JA", + level = Level(1, "Beginner"), + speakers = listOf( + createDummySpeaker(), + createDummySpeaker() + ), + isFavorited = true + ) +} + +fun createDummySpeaker(): Speaker { return Speaker( name = "tm", imageUrl = "http://example.com", @@ -36,3 +45,52 @@ private fun createSpeaker(): Speaker { companyUrl = null ) } + + +fun createDummySpeakerEntities(): List { + return listOf( + SpeakerEntity( + "aaaa" + , "hogehoge" + , "https://example.com" + , "http://example.com/hoge" + , null + , null + , "http://example.github.com/hoge" + ), + SpeakerEntity( + "bbbb" + , "hogehuga" + , "https://example.com" + , "http://example.com/hoge" + , null + , null + , "http://example.github.com/hoge" + )) +} + +fun createDummySessionWithSpeakersEntities(): List { + return listOf(SessionWithSpeakers(SessionEntity(DUMMY_SESSION_ID1, + DUMMY_SESSION_TITLE1, + "Endless battle", + LocalDateTime.of(1, 1, 1, 1, 1), + LocalDateTime.of(1, 1, 1, 1, 1), + "30分", + "日本語", + LevelEntity(1, "ニッチ / Niche"), + TopicEntity(1, "開発環境 / Development"), + RoomEntity(1, "ホール")), + listOf("aaaa", "bbbb")), + SessionWithSpeakers(SessionEntity(DUMMY_SESSION_ID2, + DUMMY_SESSION_TITLE2, + "Endless battle", + LocalDateTime.of(1, 1, 1, 1, 1), + LocalDateTime.of(1, 1, 1, 1, 1), + "30分", + "日本語", + LevelEntity(1, "ニッチ / Niche"), + TopicEntity(1, "開発環境 / Development"), + RoomEntity(1, "ホール")), + listOf("aaaa", "bbbb")) + ) +} diff --git a/app/src/test/java/io/github/droidkaigi/confsched2018/data/repository/SessionsDataRepositoryTest.kt b/app/src/test/java/io/github/droidkaigi/confsched2018/data/repository/SessionsDataRepositoryTest.kt index 439f0f44..da0ba176 100644 --- a/app/src/test/java/io/github/droidkaigi/confsched2018/data/repository/SessionsDataRepositoryTest.kt +++ b/app/src/test/java/io/github/droidkaigi/confsched2018/data/repository/SessionsDataRepositoryTest.kt @@ -4,11 +4,15 @@ import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.verify import com.nhaarman.mockito_kotlin.whenever +import io.github.droidkaigi.confsched2018.DUMMY_SESSION_TITLE1 +import io.github.droidkaigi.confsched2018.createDummySessionWithSpeakersEntities +import io.github.droidkaigi.confsched2018.createDummySpeakerEntities import io.github.droidkaigi.confsched2018.data.db.FavoriteDatabase import io.github.droidkaigi.confsched2018.data.db.SessionDatabase -import io.github.droidkaigi.confsched2018.data.db.entity.* +import io.github.droidkaigi.confsched2018.data.db.entity.RoomEntity import io.github.droidkaigi.confsched2018.data.db.entity.mapper.toRooms import io.github.droidkaigi.confsched2018.data.db.entity.mapper.toSession +import io.github.droidkaigi.confsched2018.model.SearchResult import io.github.droidkaigi.confsched2018.util.rx.TestSchedulerProvider import io.reactivex.Flowable import org.junit.Before @@ -16,7 +20,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.robolectric.RobolectricTestRunner -import org.threeten.bp.LocalDateTime @RunWith(RobolectricTestRunner::class) class SessionsDataRepositoryTest { @@ -47,39 +50,8 @@ class SessionsDataRepositoryTest { } @Test fun sessions() { - val sessions = listOf(SessionWithSpeakers(SessionEntity("10" - , "DroidKaigi app" - , "Endless battle" - , LocalDateTime.of(1, 1, 1, 1, 1) - , LocalDateTime.of(1, 1, 1, 1, 1) - , "30分" - , "日本語" - , LevelEntity(1, "ニッチ / Niche") - , TopicEntity(1, "開発環境 / Development") - , RoomEntity(1, "ホール")), - listOf( - "aaaa", "bbbb" - ))) - val speakers = listOf( - SpeakerEntity( - "aaaa" - , "hogehoge" - , "https://example.com" - , "http://example.com/hoge" - , null - , null - , "http://example.github.com/hoge" - ), - SpeakerEntity( - "bbbb" - , "hogehuga" - , "https://example.com" - , "http://example.com/hoge" - , null - , null - , "http://example.github.com/hoge" - )) - + val sessions = createDummySessionWithSpeakersEntities() + val speakers = createDummySpeakerEntities() whenever(sessionDatabase.getAllSessions()).doReturn(Flowable.just(sessions)) whenever(sessionDatabase.getAllSpeaker()).doReturn(Flowable.just(speakers)) @@ -96,4 +68,25 @@ class SessionsDataRepositoryTest { verify(sessionDatabase).getAllSessions() } + @Test fun search() { + val sessions = createDummySessionWithSpeakersEntities() + val speakers = createDummySpeakerEntities() + whenever(sessionDatabase.getAllSessions()).doReturn(Flowable.just(sessions)) + whenever(sessionDatabase.getAllSpeaker()).doReturn(Flowable.just(speakers)) + val sessionDataRepository = SessionDataRepository(mock(), + sessionDatabase, + favoriteDatabase, + TestSchedulerProvider()) + + sessionDataRepository.search(DUMMY_SESSION_TITLE1) + .doOnSuccess { + println(it) + } + .test() + .assertValue(SearchResult(listOf(sessions[0].toSession(speakers, emptyList())), + listOf())) + + verify(sessionDatabase).getAllSessions() + } + } diff --git a/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/detail/SessionDetailViewModelTest.kt b/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/detail/SessionDetailViewModelTest.kt index ae305ab3..7f6b6ed1 100644 --- a/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/detail/SessionDetailViewModelTest.kt +++ b/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/detail/SessionDetailViewModelTest.kt @@ -2,7 +2,7 @@ package io.github.droidkaigi.confsched2018.presentation.detail import android.arch.lifecycle.Observer import com.nhaarman.mockito_kotlin.* -import io.github.droidkaigi.confsched2018.DUMMY_SESSION_ID_1 +import io.github.droidkaigi.confsched2018.DUMMY_SESSION_ID1 import io.github.droidkaigi.confsched2018.createDummySession import io.github.droidkaigi.confsched2018.createDummySessions import io.github.droidkaigi.confsched2018.data.repository.SessionRepository @@ -43,13 +43,13 @@ class SessionDetailViewModelTest { val sessions = createDummySessions() whenever(repository.sessions).doReturn(Flowable.just(sessions)) viewModel = SessionDetailViewModel(repository, TestSchedulerProvider()) - viewModel.sessionId = DUMMY_SESSION_ID_1 + viewModel.sessionId = DUMMY_SESSION_ID1 val result: Observer> = mock() viewModel.session.observeForever(result) verify(repository).sessions - verify(result).onChanged(Result.success(createDummySession(DUMMY_SESSION_ID_1))) + verify(result).onChanged(Result.success(createDummySession(DUMMY_SESSION_ID1))) } @Test fun sessions_Error() { diff --git a/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModelTest.kt b/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModelTest.kt new file mode 100644 index 00000000..2ac07e49 --- /dev/null +++ b/app/src/test/java/io/github/droidkaigi/confsched2018/presentation/search/SearchViewModelTest.kt @@ -0,0 +1,82 @@ +package io.github.droidkaigi.confsched2018.presentation.search + +import android.arch.lifecycle.Observer +import com.nhaarman.mockito_kotlin.* +import io.github.droidkaigi.confsched2018.createDummySessions +import io.github.droidkaigi.confsched2018.createDummySpeaker +import io.github.droidkaigi.confsched2018.data.repository.SessionRepository +import io.github.droidkaigi.confsched2018.model.SearchResult +import io.github.droidkaigi.confsched2018.model.Session +import io.github.droidkaigi.confsched2018.presentation.Result +import io.github.droidkaigi.confsched2018.util.rx.TestSchedulerProvider +import io.reactivex.Completable +import io.reactivex.Single +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SearchViewModelTest { + @Mock private val repository: SessionRepository = mock() + + private lateinit var viewModel: SearchViewModel + + @Before fun init() { + whenever(repository.refreshSessions()).doReturn(Completable.complete()) + } + + @Test fun search_Empty() { + whenever(repository.search("query")).doReturn(Single.just(SearchResult(listOf(), listOf()))) + viewModel = SearchViewModel(repository, TestSchedulerProvider()) + val result: Observer> = mock() + viewModel.result.observeForever(result) + + viewModel.onQuery("query") + + + verify(repository).search("query") + verify(result).onChanged(Result.inProgress()) + verify(result).onChanged(Result.success(SearchResult(listOf(), listOf()))) + + } + + @Test fun search_Basic() { + val searchResult = SearchResult(createDummySessions(), listOf(createDummySpeaker())) + whenever(repository.search("query")).doReturn(Single.just(searchResult)) + viewModel = SearchViewModel(repository, TestSchedulerProvider()) + val result: Observer> = mock() + viewModel.result.observeForever(result) + + viewModel.onQuery("query") + + verify(repository).search("query") + verify(result).onChanged(Result.success(searchResult)) + } + + @Test fun search_Error() { + val runtimeException = RuntimeException("test") + whenever(repository.search("query")).doReturn(Single.error(runtimeException)) + viewModel = SearchViewModel(repository, TestSchedulerProvider()) + val result: Observer> = mock() + viewModel.result.observeForever(result) + + viewModel.onQuery("query") + + verify(repository).search("query") + verify(result).onChanged(Result.failure(runtimeException.message!!, runtimeException)) + } + + + @Test fun favorite() { + whenever(repository.favorite(any())).doReturn(Single.just(true)) + viewModel = SearchViewModel(repository, TestSchedulerProvider()) + val session = mock() + + viewModel.onFavoriteClick(session) + + verify(repository).favorite(session) + } + +} diff --git a/common-jvm/src/main/kotlin/JvmDate.kt b/common-jvm/src/main/kotlin/JvmDate.kt index 88af77f2..5ae7ce01 100644 --- a/common-jvm/src/main/kotlin/JvmDate.kt +++ b/common-jvm/src/main/kotlin/JvmDate.kt @@ -32,6 +32,10 @@ actual class Date { actual fun getTime(): Number = calendar.timeInMillis + override fun hashCode(): Int { + return date.time.toInt() + } + override fun equals(other: Any?): Boolean = other is Date && other.calendar.time == calendar.time } diff --git a/common/src/main/kotlin/SearchResult.kt b/common/src/main/kotlin/SearchResult.kt new file mode 100644 index 00000000..b557ace1 --- /dev/null +++ b/common/src/main/kotlin/SearchResult.kt @@ -0,0 +1,6 @@ +package io.github.droidkaigi.confsched2018.model + +data class SearchResult ( + val sessions:List, + val speakers:List +)