diff --git a/transport-eta-android/presentation/build.gradle b/transport-eta-android/presentation/build.gradle index 562fb5d..466edc2 100644 --- a/transport-eta-android/presentation/build.gradle +++ b/transport-eta-android/presentation/build.gradle @@ -78,7 +78,6 @@ androidExtensions { dependencies { // Module implementation project(':sms') - // Javax implementation deps.javax.inject compileOnly deps.javax.annotation @@ -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 } diff --git a/transport-eta-android/presentation/src/androidTest/java/com/joaquimley/transporteta/presentation/ExampleInstrumentedTest.java b/transport-eta-android/presentation/src/androidTest/java/com/joaquimley/transporteta/presentation/ExampleInstrumentedTest.java deleted file mode 100644 index 8d8097a..0000000 --- a/transport-eta-android/presentation/src/androidTest/java/com/joaquimley/transporteta/presentation/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.joaquimley.transporteta.presentation; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.joaquimley.transporteta.presentation.test", appContext.getPackageName()); - } -} diff --git a/transport-eta-android/presentation/src/main/java/com/joaquimley/transporteta/presentation/util/Event.kt b/transport-eta-android/presentation/src/main/java/com/joaquimley/transporteta/presentation/util/Event.kt new file mode 100644 index 0000000..0456038 --- /dev/null +++ b/transport-eta-android/presentation/src/main/java/com/joaquimley/transporteta/presentation/util/Event.kt @@ -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(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 +} \ No newline at end of file diff --git a/transport-eta-android/presentation/src/main/java/com/joaquimley/transporteta/presentation/util/EventObserver.kt b/transport-eta-android/presentation/src/main/java/com/joaquimley/transporteta/presentation/util/EventObserver.kt new file mode 100644 index 0000000..c7ef1f2 --- /dev/null +++ b/transport-eta-android/presentation/src/main/java/com/joaquimley/transporteta/presentation/util/EventObserver.kt @@ -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(private val onEventUnhandledContent: (T) -> Unit) : Observer> { + override fun onChanged(event: Event?) { + event?.getContentIfNotHandled()?.let { value -> + onEventUnhandledContent(value) + } + } +} \ No newline at end of file diff --git a/transport-eta-android/presentation/src/prod/java/com/joaquimley/transporteta/presentation/home/favorite/FavoritesViewModelImpl.kt b/transport-eta-android/presentation/src/prod/java/com/joaquimley/transporteta/presentation/home/favorite/FavoritesViewModelImpl.kt index 13f5657..acf17c1 100644 --- a/transport-eta-android/presentation/src/prod/java/com/joaquimley/transporteta/presentation/home/favorite/FavoritesViewModelImpl.kt +++ b/transport-eta-android/presentation/src/prod/java/com/joaquimley/transporteta/presentation/home/favorite/FavoritesViewModelImpl.kt @@ -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())) }) diff --git a/transport-eta-android/mobile-ui/src/test/java/com/joaquimley/transporteta/ui/home/favorite/FavoritesViewModelTest.kt b/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/FavoritesViewModelTest.kt similarity index 58% rename from transport-eta-android/mobile-ui/src/test/java/com/joaquimley/transporteta/ui/home/favorite/FavoritesViewModelTest.kt rename to transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/FavoritesViewModelTest.kt index e38c91f..511d994 100644 --- a/transport-eta-android/mobile-ui/src/test/java/com/joaquimley/transporteta/ui/home/favorite/FavoritesViewModelTest.kt +++ b/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/FavoritesViewModelTest.kt @@ -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 @@ -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) @@ -30,11 +34,12 @@ class FavoritesViewModelTest { @Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule() @Mock private lateinit var smsController: SmsController - @Mock private lateinit var observer: Observer>> @Mock private lateinit var acceptingRequestsObserver: Observer + @Mock private lateinit var observer: Observer>> + @Captor private lateinit var argumentCaptor: ArgumentCaptor + + private lateinit var captor: KArgumentCaptor -// private lateinit var captor: KArgumentCaptor - private lateinit var smsTestObserver: TestObserver private lateinit var favoritesViewModel: FavoritesViewModelImpl private var testSmsModel = SmsModel(Random().nextInt(), UUID.randomUUID().toString()) @@ -42,8 +47,12 @@ class FavoritesViewModelTest { @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 @@ -51,6 +60,40 @@ class FavoritesViewModelTest { } + @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`() { @@ -58,25 +101,45 @@ class FavoritesViewModelTest { 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()) @@ -84,7 +147,9 @@ class FavoritesViewModelTest { // 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") diff --git a/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/factory/DataFactory.kt b/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/factory/DataFactory.kt new file mode 100644 index 0000000..2afa2e2 --- /dev/null +++ b/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/factory/DataFactory.kt @@ -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 { + val items: MutableList = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + +} \ No newline at end of file diff --git a/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/factory/TestFactoryFavoriteView.kt b/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/factory/TestFactoryFavoriteView.kt new file mode 100644 index 0000000..802b060 --- /dev/null +++ b/transport-eta-android/presentation/src/test/java/com/joaquimley/transporteta/presentation/factory/TestFactoryFavoriteView.kt @@ -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 { + val result = ArrayList() + for(i in 0..size) { + result.add(generateFavoriteView()) + } + return result + } +} \ No newline at end of file