1
0
Fork 0

Fixes #589: Add sample unit tests for a component

master
Colin Lee 2019-02-19 20:10:17 -06:00 committed by Jeff Boek
parent a2031b92cd
commit 5cf61c95db
9 changed files with 170 additions and 26 deletions

View File

@ -65,6 +65,16 @@ android {
}
}
android.applicationVariants.all { variant ->
boolean hasTest = gradle.startParameter.taskNames.find {it.contains("test") || it.contains("Test")} != null
if (hasTest) {
apply plugin: 'kotlin-allopen'
allOpen {
annotation("org.mozilla.fenix.test.Mockable")
}
}
}
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name
@ -152,10 +162,6 @@ dependencies {
debugImplementation Deps.leakcanary
releaseImplementation Deps.leakcanary_noop
testImplementation Deps.junit
androidTestImplementation Deps.tools_test_runner
androidTestImplementation Deps.tools_espresso_core
armImplementation Deps.geckoview_nightly_arm
x86Implementation Deps.geckoview_nightly_x86
aarch64Implementation Deps.geckoview_nightly_aarch64
@ -167,6 +173,17 @@ dependencies {
implementation Deps.android_arch_navigation_ui
implementation Deps.autodispose
androidTestImplementation Deps.tools_test_runner
androidTestImplementation Deps.tools_espresso_core
testImplementation Deps.junit_jupiter_api
testImplementation Deps.junit_jupiter_params
testImplementation Deps.junit_jupiter_engine
testImplementation Deps.mockito_core
androidTestImplementation Deps.mockito_android
testImplementation Deps.mockk
}

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.library.history
import android.view.ViewGroup
import org.mozilla.fenix.test.Mockable
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
@ -25,6 +26,7 @@ data class HistoryItem(val id: Int, val url: String) {
}
}
@Mockable
class HistoryComponent(
private val container: ViewGroup,
bus: ActionBusFactory,

View File

@ -1,17 +0,0 @@
package org.mozilla.fenix
import org.junit.Test
/* ktlint-disable no-wildcard-imports */
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix
import androidx.lifecycle.LifecycleOwner
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.Schedulers
import org.mozilla.fenix.mvi.ActionBusFactory
object TestUtils {
fun setRxSchedulers() {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
}
val owner = mockk<LifecycleOwner> {
every { lifecycle } returns mockk()
every { lifecycle.addObserver(any()) } just Runs
}
val bus: ActionBusFactory = ActionBusFactory.get(owner)
}

View File

@ -0,0 +1,90 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.history
import android.view.ViewGroup
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.spyk
import io.reactivex.Observer
import io.reactivex.observers.TestObserver
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mozilla.fenix.TestUtils.bus
import org.mozilla.fenix.TestUtils.owner
import org.mozilla.fenix.TestUtils.setRxSchedulers
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.getManagedEmitter
class HistoryComponentTest {
private lateinit var historyComponent: TestHistoryComponent
private lateinit var historyObserver: TestObserver<HistoryState>
private lateinit var emitter: Observer<HistoryChange>
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
setRxSchedulers()
historyComponent = spyk(
TestHistoryComponent(mockk(), bus),
recordPrivateCalls = true
)
historyObserver = historyComponent.internalRender(historyComponent.reducer).test()
emitter = owner.getManagedEmitter()
}
@Test
fun `add and remove one history item normally`() {
val historyItem = HistoryItem(123, "http://mozilla.org")
emitter.onNext(HistoryChange.Change(listOf(historyItem)))
emitter.onNext(HistoryChange.EnterEditMode(historyItem))
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem))
emitter.onNext(HistoryChange.ExitEditMode)
historyObserver.assertSubscribed().awaitCount(6).assertNoErrors()
.assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf())),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem), HistoryState.Mode.Normal)
)
}
@Test
fun `try making changes when not in edit mode`() {
val historyItems = listOf(
HistoryItem(1337, "http://reddit.com"),
HistoryItem(31337, "http://leethaxor.com")
)
emitter.onNext(HistoryChange.Change(historyItems))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItems[0]))
emitter.onNext(HistoryChange.EnterEditMode(historyItems[0]))
emitter.onNext(HistoryChange.ExitEditMode)
historyObserver.assertSubscribed().awaitCount(4).assertNoErrors()
.assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(historyItems, HistoryState.Mode.Normal),
HistoryState(historyItems, HistoryState.Mode.Editing(listOf(historyItems[0]))),
HistoryState(historyItems, HistoryState.Mode.Normal)
)
}
@Suppress("MemberVisibilityCanBePrivate")
class TestHistoryComponent(container: ViewGroup, bus: ActionBusFactory) :
HistoryComponent(container, bus) {
override val uiView: UIView<HistoryState, HistoryAction, HistoryChange>
get() = mockk(relaxed = true)
}
}

View File

@ -17,19 +17,22 @@ abstract class UIComponent<S : ViewState, A : Action, C : Change>(
abstract var initialState: S
abstract val reducer: Reducer<S, C>
val uiView: UIView<S, A, C> by lazy { initView() }
open val uiView: UIView<S, A, C> by lazy { initView() }
abstract fun initView(): UIView<S, A, C>
open fun getContainerId() = uiView.containerId
/**
* Render the ViewState to the View through the Reducer
*/
fun render(reducer: Reducer<S, C>): Disposable =
internalRender(reducer)
.subscribe(uiView.updateView())
fun internalRender(reducer: Reducer<S, C>): Observable<S> =
changesObservable
.scan(initialState, reducer)
.distinctUntilChanged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(uiView.updateView())
}

View File

@ -0,0 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.test
annotation class Mockable

View File

@ -9,6 +9,7 @@ buildscript {
classpath Deps.tools_androidgradle
classpath Deps.tools_kotlingradle
classpath Deps.androidx_safeargs
classpath Deps.allopen
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -5,6 +5,7 @@
private object Versions {
const val kotlin = "1.3.11"
const val android_gradle_plugin = "3.2.1"
const val geckoNightly = "67.0.20190213102848"
const val rxAndroid = "2.1.0"
const val rxKotlin = "2.3.0"
@ -23,13 +24,16 @@ private object Versions {
const val mozilla_android_components = "0.43.0-SNAPSHOT"
const val junit = "4.12"
const val test_tools = "1.0.2"
const val espresso_core = "2.2.2"
const val android_arch_navigation = "1.0.0-beta02"
const val autodispose = "1.1.0"
const val junit_jupiter = "5.3.2"
const val mockito = "2.23.0"
const val mockk = "1.9.kotlin12"
}
@Suppress("unused")
@ -38,6 +42,8 @@ object Deps {
const val tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
const val allopen = "org.jetbrains.kotlin:kotlin-allopen:${Versions.kotlin}"
const val rxKotlin = "io.reactivex.rxjava2:rxkotlin:${Versions.rxKotlin}"
const val rxAndroid = "io.reactivex.rxjava2:rxandroid:${Versions.rxAndroid}"
@ -96,7 +102,6 @@ object Deps {
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}"
const val leakcanary_noop = "com.squareup.leakcanary:leakcanary-android-no-op:${Versions.leakcanary}"
const val junit = "junit:junit:${Versions.junit}"
const val tools_test_runner = "com.android.support.test:runner:${Versions.test_tools}"
const val tools_espresso_core = "com.android.support.test.espresso:espresso-core:${Versions.espresso_core}"
@ -115,5 +120,13 @@ object Deps {
const val autodispose_android = "com.uber.autodispose:autodispose-android:${Versions.autodispose}"
const val autodispose_android_aac = "com.uber.autodispose:autodispose-android-archcomponents:${Versions.autodispose}"
const val autodispose_android_aac_test = "com.uber.autodispose:autodispose-android-archcomponents-test:${Versions.autodispose}"
const val junit_jupiter_api = "org.junit.jupiter:junit-jupiter-api:${Versions.junit_jupiter}"
const val junit_jupiter_params = "org.junit.jupiter:junit-jupiter-params:${Versions.junit_jupiter}"
const val junit_jupiter_engine = "org.junit.jupiter:junit-jupiter-engine:${Versions.junit_jupiter}"
const val mockito_core = "org.mockito:mockito-core:${Versions.mockito}"
const val mockito_android = "org.mockito:mockito-android:${Versions.mockito}"
const val mockk = "io.mockk:mockk:${Versions.mockk}"
}