Skip to content

Commit

Permalink
Fix some unit tests for FavoritesViewModel
Browse files Browse the repository at this point in the history
From now on, the only solution is to star creating a data source to
the ViewModel in order to properly mock data.

#29
  • Loading branch information
JoaquimLey committed Apr 27, 2018
1 parent 5740ac7 commit 82f4631
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 42 deletions.
12 changes: 11 additions & 1 deletion transport-eta-android/presentation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ androidExtensions {
dependencies {
// Module
implementation project(':sms')

// Javax
implementation deps.javax.inject
compileOnly deps.javax.annotation
Expand All @@ -91,4 +90,15 @@ dependencies {
// ACC
kapt deps.lifecycle.compiler
implementation deps.lifecycle.extensions
// Local unit tests
kaptTest deps.dagger.compiler
testImplementation deps.junit
testImplementation deps.hamcrest
testImplementation deps.kotlin.test
testImplementation deps.mockito.kotlin
testImplementation deps.mockito.inline
testImplementation deps.arch_core.testing
// Resolve conflicts between main and local unit tests
testImplementation deps.support.core_utils
testImplementation deps.support.annotations
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.joaquimley.transporteta.presentation.util

/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {

var hasBeenHandled = false
private set // Allow external read but not write

/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.joaquimley.transporteta.presentation.util

import android.arch.lifecycle.Observer

/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ class FavoritesViewModelImpl @Inject constructor(smsController: SmsController) :
} else {
data.add(newFavoriteView)
}
// TODO (possible caching this to local storage at this point)
// TODO Possible caching this to local storage at this point (when mapper is used)
// TODO And have favouritesLiveData actually bound to the cache instead of posting like this

favouritesLiveData.postValue(Resource.success(data))
}, { favouritesLiveData.postValue(Resource.error(it.message.orEmpty())) })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.joaquimley.transporteta.ui.home.favorite
package com.joaquimley.transporteta.presentation

import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.Observer
Expand All @@ -7,21 +7,25 @@ import com.joaquimley.transporteta.presentation.home.favorite.FavoritesViewModel
import com.joaquimley.transporteta.presentation.model.FavoriteView
import com.joaquimley.transporteta.sms.SmsController
import com.joaquimley.transporteta.sms.model.SmsModel
import com.joaquimley.transporteta.ui.model.data.ResourceState
import com.joaquimley.transporteta.ui.testing.factory.TestFactoryFavoriteView
import com.nhaarman.mockito_kotlin.KArgumentCaptor
import com.nhaarman.mockito_kotlin.atLeastOnce
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import io.reactivex.Single
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.observers.TestObserver
import io.reactivex.schedulers.Schedulers
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.*
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnitRunner
import java.util.*
import kotlin.test.assertEquals


@RunWith(MockitoJUnitRunner::class)
Expand All @@ -30,61 +34,122 @@ class FavoritesViewModelTest {
@Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()

@Mock private lateinit var smsController: SmsController
@Mock private lateinit var observer: Observer<Resource<List<FavoriteView>>>
@Mock private lateinit var acceptingRequestsObserver: Observer<Boolean>
@Mock private lateinit var observer: Observer<Resource<List<FavoriteView>>>
@Captor private lateinit var argumentCaptor: ArgumentCaptor<Int>

private lateinit var captor: KArgumentCaptor<Int>

// private lateinit var captor: KArgumentCaptor<SmsController>
private lateinit var smsTestObserver: TestObserver<SmsModel>
private lateinit var favoritesViewModel: FavoritesViewModelImpl

private var testSmsModel = SmsModel(Random().nextInt(), UUID.randomUUID().toString())

@Before
fun setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
`when`(smsController.requestEta(anyInt())).thenReturn(Single.just(testSmsModel))
favoritesViewModel = FavoritesViewModelImpl(smsController)

favoritesViewModel.getFavourites().observeForever(observer)
favoritesViewModel.getAcceptingRequests().observeForever(acceptingRequestsObserver)
captor = KArgumentCaptor(argumentCaptor, Int::class)
`when`(smsController.requestEta(anyInt())).thenReturn(Single.just(testSmsModel))
}

@After
fun tearDown() {

}

@Test
fun `fetch eta triggers not accepting requests state`() {
// given
val favoriteView = FavoriteView(Random().nextInt(), UUID.randomUUID().toString(), UUID.randomUUID().toString())
// when
favoritesViewModel.onEtaRequested(favoriteView)
// then
verify(acceptingRequestsObserver).onChanged(false)
}

@Test
@Throws(IllegalArgumentException::class)
fun `when favorite eta request is canceled accepting requests is true`() {
// given
val favoriteView = FavoriteView(Random().nextInt(), UUID.randomUUID().toString(), UUID.randomUUID().toString())
favoritesViewModel.onEtaRequested(favoriteView)
// when
favoritesViewModel.cancelEtaRequest()
// then
verify(acceptingRequestsObserver, atLeastOnce()).onChanged(true)
}

@Test
@Throws(IllegalArgumentException::class)
fun `when favorite eta request is canceled smsController invalidate request is called`() {
// given
val favoriteView = FavoriteView(Random().nextInt(), UUID.randomUUID().toString(), UUID.randomUUID().toString())
favoritesViewModel.onEtaRequested(favoriteView)
// when
favoritesViewModel.cancelEtaRequest()
// then
verify(smsController, times(1)).invalidateRequest()
}

@Test
@Throws(IllegalArgumentException::class)
fun `when favorite eta is requested correct favorite code is passed to smsController`() {
// given
val favoriteView = FavoriteView(Random().nextInt(), UUID.randomUUID().toString(), UUID.randomUUID().toString())
// when
favoritesViewModel.onEtaRequested(favoriteView)
verify(smsController).requestEta(captor.capture())
// then
verify(smsController, times(1)).requestEta(favoriteView.code)
assertThat(captor.firstValue, `is`(favoriteView.code))
}

@Test
fun `fetch eta triggers not accepting requests state`() {
@Throws(IllegalArgumentException::class)
fun `when favorite eta is requested correct smsController requestEta is called`() {
// given
favoritesViewModel.getAcceptingRequests().observeForever(acceptingRequestsObserver)
val favoriteView = FavoriteView(Random().nextInt(), UUID.randomUUID().toString(), UUID.randomUUID().toString())
// when
favoritesViewModel.onEtaRequested(favoriteView)
// then
verify(acceptingRequestsObserver).onChanged(false)
verify(smsController, times(1)).requestEta(anyInt())
}

@Test
@Throws(IllegalArgumentException::class)
fun `when sms is received triggers accepting requests state`() {
favoritesViewModel.getAcceptingRequests().observeForever(acceptingRequestsObserver)

/**
val list = BufferooFactory.makeBufferooList(2)
val viewList = BufferooFactory.makeBufferooViewList(2)
stubBufferooMapperMapToView(viewList[0], list[0])
stubBufferooMapperMapToView(viewList[1], list[1])
bufferoosViewModel.getBufferoos()
verify(getBufferoos).execute(captor.capture(), eq(null))
captor.firstValue.onNext(list)
assert(bufferoosViewModel.getBufferoos().value?.status == ResourceState.SUCCESS)
*/
val results = TestFactoryFavoriteView.generateFavoriteViewList()

// given
val favoriteView = FavoriteView(Random().nextInt(), UUID.randomUUID().toString(), UUID.randomUUID().toString())
val testSms = SmsModel(Random().nextInt(), UUID.randomUUID().toString())
`when`(smsController.requestEta(favoriteView.code)).thenReturn(Single.just(testSms))
// when
favoritesViewModel.onEtaRequested(favoriteView)
// then
verify(acceptingRequestsObserver).onChanged(true)

// verify(observer).onChanged()

}

@Ignore("Ignored test: when sms is received correct data is passed -> Lacking implementation")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.joaquimley.transporteta.ui.testing.factory.ui

import java.util.concurrent.ThreadLocalRandom

/**
* Factory class for data instances
*/

object DataFactory {

fun randomString(): String {
return "Name ${java.util.UUID.randomUUID()}"
}

fun randomUuid(): String {
return java.util.UUID.randomUUID().toString()
}

fun randomInt(): Int {
return ThreadLocalRandom.current().nextInt(0, 1000 + 1)
}

fun randomLong(): Long {
return randomInt().toLong()
}

fun randomBoolean(): Boolean {
return Math.random() < 0.5
}

fun makeStringList(count: Int): List<String> {
val items: MutableList<String> = mutableListOf()
repeat(count) {
items.add(randomUuid())
}
return items
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.joaquimley.transporteta.ui.testing.factory

import android.support.annotation.RestrictTo
import com.joaquimley.transporteta.presentation.model.FavoriteView
import com.joaquimley.transporteta.ui.testing.factory.ui.DataFactory

@RestrictTo(RestrictTo.Scope.TESTS)
object TestFactoryFavoriteView {

fun generateFavoriteView(busStopCode: Int? = null): FavoriteView {
return FavoriteView(busStopCode
?: DataFactory.randomInt(), DataFactory.randomString(), DataFactory.randomString())
}

fun generateFavoriteViewList(size: Int = 5): List<FavoriteView> {
val result = ArrayList<FavoriteView>()
for(i in 0..size) {
result.add(generateFavoriteView())
}
return result
}
}

0 comments on commit 82f4631

Please sign in to comment.