Skip to content

Commit

Permalink
Adds TestCoroutineDispatcher + LiveData coroutines support (nickbutch…
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelvicnt authored Aug 27, 2019
1 parent 942275c commit 9aa03c4
Show file tree
Hide file tree
Showing 17 changed files with 211 additions and 83 deletions.
4 changes: 4 additions & 0 deletions about/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

dynamicFeatures = [':about', ':designernews', ':dribbble', ':search']
}

Expand Down
11 changes: 5 additions & 6 deletions app/src/main/java/io/plaidapp/ui/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ package io.plaidapp.ui

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import io.plaidapp.core.data.CoroutinesDispatcherProvider
import io.plaidapp.core.data.DataLoadingSubject
Expand Down Expand Up @@ -107,12 +108,10 @@ class HomeViewModel(
loadData()
}

fun getFeed(columns: Int): LiveData<FeedUiModel> {
return Transformations.switchMap(feedData) {
// TODO move this on a background thread
// https://github.com/nickbutcher/plaid/issues/658
fun getFeed(columns: Int) = feedData.switchMap {
liveData(viewModelScope.coroutineContext + dispatcherProvider.computation) {
expandPopularItems(it, columns)
return@switchMap MutableLiveData(FeedUiModel(it))
emit(FeedUiModel(it))
}
}

Expand Down
26 changes: 15 additions & 11 deletions app/src/test/java/io/plaidapp/ui/HomeViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ import io.plaidapp.dribbbleSource
import io.plaidapp.post
import io.plaidapp.shot
import io.plaidapp.story
import io.plaidapp.test.shared.CoroutinesMainDispatcherRule
import io.plaidapp.test.shared.MainCoroutineRule
import io.plaidapp.test.shared.LiveDataTestUtil
import io.plaidapp.test.shared.provideFakeCoroutinesDispatcherProvider
import io.plaidapp.test.shared.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
Expand All @@ -57,10 +59,12 @@ import org.mockito.MockitoAnnotations
/**
* Tests for [HomeViewModel], with dependencies mocked.
*/
@ExperimentalCoroutinesApi
class HomeViewModelTest {

// Set the main coroutines dispatcher for unit testing
@get:Rule
var coroutinesMainDispatcherRule = CoroutinesMainDispatcherRule()
var coroutinesRule = MainCoroutineRule()

// Executes tasks in the Architecture Components in the same thread
@get:Rule
Expand Down Expand Up @@ -278,7 +282,7 @@ class HomeViewModelTest {
}

@Test
fun filtersRemoved() {
fun filtersRemoved() = coroutinesRule.runBlocking {
// Given a view model with feed data
val homeViewModel = createViewModelWithFeedData(listOf(post, shot, story))
verify(sourcesRepository).registerFilterChangedCallback(
Expand All @@ -294,7 +298,7 @@ class HomeViewModelTest {
}

@Test
fun filtersChanged_activeSource() {
fun filtersChanged_activeSource() = coroutinesRule.runBlocking {
// Given a view model with feed data
val homeViewModel = createViewModelWithFeedData(listOf(post, shot, story))
verify(sourcesRepository).registerFilterChangedCallback(
Expand All @@ -312,7 +316,7 @@ class HomeViewModelTest {
}

@Test
fun filtersChanged_inactiveSource() {
fun filtersChanged_inactiveSource() = coroutinesRule.runBlocking {
// Given a view model with feed data
val homeViewModel = createViewModelWithFeedData(listOf(post, shot, story))
verify(sourcesRepository).registerFilterChangedCallback(
Expand Down Expand Up @@ -357,7 +361,7 @@ class HomeViewModelTest {
}

@Test
fun dataLoading_atInit() = runBlocking {
fun dataLoading_atInit() = coroutinesRule.runBlocking {
// When creating a view model
createViewModel()

Expand All @@ -366,7 +370,7 @@ class HomeViewModelTest {
}

@Test
fun feed_emitsWhenDataLoaded() {
fun feed_emitsWhenDataLoaded() = coroutinesRule.runBlocking {
// Given a view model
val homeViewModel = createViewModel()
verify(dataManager).setOnDataLoadedCallback(capture(dataLoadedCallback))
Expand All @@ -391,13 +395,13 @@ class HomeViewModelTest {

private fun createViewModel(
list: List<SourceItem> = emptyList()
): HomeViewModel = runBlocking {
whenever(sourcesRepository.getSources()).thenReturn(list)
return@runBlocking HomeViewModel(
): HomeViewModel {
runBlocking { whenever(sourcesRepository.getSources()).thenReturn(list) }
return HomeViewModel(
dataManager,
loginRepository,
sourcesRepository,
provideFakeCoroutinesDispatcherProvider()
provideFakeCoroutinesDispatcherProvider(coroutinesRule.testDispatcher)
)
}
}
12 changes: 5 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ buildscript { scriptHandler ->
'appcompat' : '1.1.0-rc01',
'androidx' : '1.0.0',
'androidxCollection' : '1.0.0',
'androidxCoreRuntime': '2.0.1-alpha01',
'androidxArch' : '2.0.0',
'constraintLayout' : '2.0.0-alpha2',
'coreKtx' : '1.0.0',
'coroutines' : '1.1.1',
'coreKtx' : '1.2.0-alpha03',
'coroutines' : '1.3.0',
'crashlytics' : '2.10.1',
'dagger' : '2.23.2',
'espresso' : '3.1.0-beta02',
Expand All @@ -40,10 +39,10 @@ buildscript { scriptHandler ->
'gson' : '2.8.5',
'jsoup' : '1.11.3',
'junit' : '4.12',
'kotlin' : '1.3.41',
'kotlin' : '1.3.50',
'ktlint' : '0.24.0',
'legacyCoreUtils' : '1.0.0',
'lifecycle' : '2.1.0-alpha01',
'lifecycle' : '2.2.0-alpha03',
'material' : '1.1.0-alpha05',
'mockito' : '2.23.0',
'mockito_kotlin' : '2.0.0-RC3',
Expand All @@ -61,12 +60,11 @@ buildscript { scriptHandler ->
]

dependencies {
classpath 'com.android.tools.build:gradle:3.6.0-alpha04'
classpath 'com.android.tools.build:gradle:3.6.0-alpha07'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "com.google.gms:google-services:${versions.googleServices}"
classpath "io.fabric.tools:gradle:${versions.fabric}"
}

}

plugins {
Expand Down
4 changes: 4 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

packagingOptions {
exclude 'META-INF/core_debug.kotlin_module'
}
Expand Down
4 changes: 4 additions & 0 deletions designernews/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
Expand Down
4 changes: 4 additions & 0 deletions dribbble/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ import io.plaidapp.dribbble.testShot
import io.plaidapp.dribbble.testShotUiModel
import io.plaidapp.test.shared.LiveDataTestUtil
import io.plaidapp.test.shared.provideFakeCoroutinesDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.TestCoroutineDispatcher
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test

/**
* Tests for [ShotViewModel], mocking out its dependencies.
*/
@ExperimentalCoroutinesApi
class ShotViewModelTest {

// Executes tasks in the Architecture Components in the same thread
Expand All @@ -53,6 +59,12 @@ class ShotViewModelTest {
private val createShotUiModel: CreateShotUiModelUseCase = mock {
on { runBlocking { invoke(any()) } } doReturn testShotUiModel
}
private val testCoroutineDispatcher = TestCoroutineDispatcher()

@After
fun tearDown() {
testCoroutineDispatcher.cleanupTestCoroutines()
}

@Test
fun loadShot_existsInRepo() {
Expand Down Expand Up @@ -86,7 +98,7 @@ class ShotViewModelTest {
// Given a view model with a shot with a known URL
val url = "https://dribbble.com/shots/2344334-Plaid-Product-Icon"
val mockShotUiModel = mock<ShotUiModel> { on { this.url } doReturn url }
runBlocking { whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel) }
whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel)
val viewModel = withViewModel(shot = testShot.copy(htmlUrl = url))

// When there is a request to view the shot
Expand Down Expand Up @@ -143,6 +155,26 @@ class ShotViewModelTest {
assertEquals(id, shotId)
}

@Test
fun loadShot_emitsTwoUiModels() = testCoroutineDispatcher.runBlockingTest {
// Given coroutines have not started yet and the View Model is created
testCoroutineDispatcher.pauseDispatcher()
val viewModel = withViewModel()

// Then the fast result has been emitted
val fastResult: ShotUiModel? = LiveDataTestUtil.getValue(viewModel.shotUiModel)
assertNotNull(fastResult)
assertTrue(fastResult!!.formattedDescription.isEmpty())

// When the coroutine starts
testCoroutineDispatcher.resumeDispatcher()

// Then the slow result has been emitted
val slowResult: ShotUiModel? = LiveDataTestUtil.getValue(viewModel.shotUiModel)
assertNotNull(slowResult)
assertTrue(slowResult!!.formattedDescription.isNotEmpty())
}

private fun withViewModel(
shot: Shot = testShot,
shareInfo: ShareShotInfo? = null
Expand All @@ -158,7 +190,8 @@ class ShotViewModelTest {
repo,
createShotUiModel,
getShareShotInfoUseCase,
provideFakeCoroutinesDispatcherProvider()
provideFakeCoroutinesDispatcherProvider(testCoroutineDispatcher,
testCoroutineDispatcher, testCoroutineDispatcher)
)
}
}
4 changes: 4 additions & 0 deletions search/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
Expand Down
17 changes: 11 additions & 6 deletions search/src/main/java/io/plaidapp/search/ui/SearchViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ package io.plaidapp.search.ui

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import io.plaidapp.core.data.CoroutinesDispatcherProvider
import io.plaidapp.core.data.PlaidItem
import io.plaidapp.core.feed.FeedProgressUiModel
import io.plaidapp.core.feed.FeedUiModel
import io.plaidapp.search.domain.SearchDataSourceFactoriesRegistry
Expand All @@ -43,13 +46,15 @@ class SearchViewModel(

private val searchQuery = MutableLiveData<String>()

private val results = Transformations.switchMap(searchQuery) {
loadSearchData = LoadSearchDataUseCase(factories, it)
loadMore()
return@switchMap loadSearchData?.searchResult
private val results: LiveData<List<PlaidItem>> = searchQuery.switchMap {
liveData(viewModelScope.coroutineContext + dispatcherProvider.computation) {
loadSearchData = LoadSearchDataUseCase(factories, it)
loadMore()
emitSource(loadSearchData!!.searchResult)
}
}

val searchResults: LiveData<FeedUiModel> = Transformations.map(results) {
val searchResults: LiveData<FeedUiModel> = results.map {
FeedUiModel(it)
}

Expand Down
19 changes: 14 additions & 5 deletions search/src/test/java/io/plaidapp/search/ui/SearchViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import io.plaidapp.core.interfaces.SearchDataSourceFactory
import io.plaidapp.search.domain.SearchDataSourceFactoriesRegistry
import io.plaidapp.search.shots
import io.plaidapp.search.testShot1
import io.plaidapp.test.shared.MainCoroutineRule
import io.plaidapp.test.shared.LiveDataTestUtil
import io.plaidapp.test.shared.provideFakeCoroutinesDispatcherProvider
import kotlinx.coroutines.runBlocking
import io.plaidapp.test.shared.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
Expand All @@ -39,8 +41,13 @@ import org.mockito.MockitoAnnotations
/**
* Tests for [SearchViewModel] that mocks the dependencies
*/
@ExperimentalCoroutinesApi
class SearchViewModelTest {

// Set the main coroutines dispatcher for unit testing
@get:Rule
var coroutinesRule = MainCoroutineRule()

// Executes tasks in the Architecture Components in the same thread
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
Expand All @@ -55,13 +62,14 @@ class SearchViewModelTest {
}

@Test
fun searchFor_searchesInDataManager() = runBlocking {
fun searchFor_searchesInDataManager() = coroutinesRule.runBlocking {
// Given a query
val query = "Plaid"
// And an expected success result
val result = Result.Success(shots)
factory.dataSource.result = result
val viewModel = SearchViewModel(registry, provideFakeCoroutinesDispatcherProvider())
val viewModel = SearchViewModel(registry,
provideFakeCoroutinesDispatcherProvider(coroutinesRule.testDispatcher))

// When searching for the query
viewModel.searchFor(query)
Expand All @@ -72,10 +80,11 @@ class SearchViewModelTest {
}

@Test
fun loadMore_loadsInDataManager() = runBlocking {
fun loadMore_loadsInDataManager() = coroutinesRule.runBlocking {
// Given a query
val query = "Plaid"
val viewModel = SearchViewModel(registry, provideFakeCoroutinesDispatcherProvider())
val viewModel = SearchViewModel(registry,
provideFakeCoroutinesDispatcherProvider(coroutinesRule.testDispatcher))
// And a search for the query
viewModel.searchFor(query)
// Given a result
Expand Down
Loading

0 comments on commit 9aa03c4

Please sign in to comment.