Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds TestCoroutineDispatcher + LiveData coroutines support #695

Merged
merged 22 commits into from Aug 27, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to鈥
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -46,6 +46,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
@@ -71,6 +71,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

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

@@ -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
@@ -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))
}
}

@@ -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
@@ -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
@@ -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(
@@ -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(
@@ -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(
@@ -357,7 +361,7 @@ class HomeViewModelTest {
}

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

@@ -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))
@@ -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)
)
}
}
@@ -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',
@@ -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',
@@ -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 {
@@ -61,6 +61,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

packagingOptions {
exclude 'META-INF/core_debug.kotlin_module'
}
@@ -48,6 +48,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
@@ -47,6 +47,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
@@ -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
@@ -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() {
@@ -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
@@ -143,6 +155,26 @@ class ShotViewModelTest {
assertEquals(id, shotId)
}

@Test
fun loadShot_emitsTwoUiModels() = testCoroutineDispatcher.runBlockingTest {
This conversation was marked as resolved by manuelvicnt

This comment has been minimized.

Copy link
@nickbutcher

nickbutcher Aug 27, 2019

Collaborator

Nice!

// 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
@@ -158,7 +190,8 @@ class ShotViewModelTest {
repo,
createShotUiModel,
getShareShotInfoUseCase,
provideFakeCoroutinesDispatcherProvider()
provideFakeCoroutinesDispatcherProvider(testCoroutineDispatcher,
testCoroutineDispatcher, testCoroutineDispatcher)
)
}
}
@@ -40,6 +40,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
@@ -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
@@ -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)
}

@@ -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
@@ -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()
@@ -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)
@@ -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
ProTip! Use n and p to navigate between commits in a pull request.
You can鈥檛 perform that action at this time.