Fixes #589: Add sample unit tests for a component
parent
a2031b92cd
commit
5cf61c95db
|
@ -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 ->
|
android.applicationVariants.all { variant ->
|
||||||
def buildType = variant.buildType.name
|
def buildType = variant.buildType.name
|
||||||
|
|
||||||
|
@ -152,10 +162,6 @@ dependencies {
|
||||||
debugImplementation Deps.leakcanary
|
debugImplementation Deps.leakcanary
|
||||||
releaseImplementation Deps.leakcanary_noop
|
releaseImplementation Deps.leakcanary_noop
|
||||||
|
|
||||||
testImplementation Deps.junit
|
|
||||||
androidTestImplementation Deps.tools_test_runner
|
|
||||||
androidTestImplementation Deps.tools_espresso_core
|
|
||||||
|
|
||||||
armImplementation Deps.geckoview_nightly_arm
|
armImplementation Deps.geckoview_nightly_arm
|
||||||
x86Implementation Deps.geckoview_nightly_x86
|
x86Implementation Deps.geckoview_nightly_x86
|
||||||
aarch64Implementation Deps.geckoview_nightly_aarch64
|
aarch64Implementation Deps.geckoview_nightly_aarch64
|
||||||
|
@ -167,6 +173,17 @@ dependencies {
|
||||||
implementation Deps.android_arch_navigation_ui
|
implementation Deps.android_arch_navigation_ui
|
||||||
|
|
||||||
implementation Deps.autodispose
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package org.mozilla.fenix.library.history
|
package org.mozilla.fenix.library.history
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import org.mozilla.fenix.test.Mockable
|
||||||
import org.mozilla.fenix.mvi.Action
|
import org.mozilla.fenix.mvi.Action
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||||
import org.mozilla.fenix.mvi.Change
|
import org.mozilla.fenix.mvi.Change
|
||||||
|
@ -25,6 +26,7 @@ data class HistoryItem(val id: Int, val url: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mockable
|
||||||
class HistoryComponent(
|
class HistoryComponent(
|
||||||
private val container: ViewGroup,
|
private val container: ViewGroup,
|
||||||
bus: ActionBusFactory,
|
bus: ActionBusFactory,
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,19 +17,22 @@ abstract class UIComponent<S : ViewState, A : Action, C : Change>(
|
||||||
|
|
||||||
abstract var initialState: S
|
abstract var initialState: S
|
||||||
abstract val reducer: Reducer<S, C>
|
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>
|
abstract fun initView(): UIView<S, A, C>
|
||||||
open fun getContainerId() = uiView.containerId
|
open fun getContainerId() = uiView.containerId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the ViewState to the View through the Reducer
|
* Render the ViewState to the View through the Reducer
|
||||||
*/
|
*/
|
||||||
fun render(reducer: Reducer<S, C>): Disposable =
|
fun render(reducer: Reducer<S, C>): Disposable =
|
||||||
|
internalRender(reducer)
|
||||||
|
.subscribe(uiView.updateView())
|
||||||
|
|
||||||
|
fun internalRender(reducer: Reducer<S, C>): Observable<S> =
|
||||||
changesObservable
|
changesObservable
|
||||||
.scan(initialState, reducer)
|
.scan(initialState, reducer)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(uiView.updateView())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -9,6 +9,7 @@ buildscript {
|
||||||
classpath Deps.tools_androidgradle
|
classpath Deps.tools_androidgradle
|
||||||
classpath Deps.tools_kotlingradle
|
classpath Deps.tools_kotlingradle
|
||||||
classpath Deps.androidx_safeargs
|
classpath Deps.androidx_safeargs
|
||||||
|
classpath Deps.allopen
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
private object Versions {
|
private object Versions {
|
||||||
const val kotlin = "1.3.11"
|
const val kotlin = "1.3.11"
|
||||||
const val android_gradle_plugin = "3.2.1"
|
const val android_gradle_plugin = "3.2.1"
|
||||||
|
|
||||||
const val geckoNightly = "67.0.20190213102848"
|
const val geckoNightly = "67.0.20190213102848"
|
||||||
const val rxAndroid = "2.1.0"
|
const val rxAndroid = "2.1.0"
|
||||||
const val rxKotlin = "2.3.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 mozilla_android_components = "0.43.0-SNAPSHOT"
|
||||||
|
|
||||||
const val junit = "4.12"
|
|
||||||
const val test_tools = "1.0.2"
|
const val test_tools = "1.0.2"
|
||||||
const val espresso_core = "2.2.2"
|
const val espresso_core = "2.2.2"
|
||||||
|
|
||||||
const val android_arch_navigation = "1.0.0-beta02"
|
const val android_arch_navigation = "1.0.0-beta02"
|
||||||
|
|
||||||
const val autodispose = "1.1.0"
|
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")
|
@Suppress("unused")
|
||||||
|
@ -38,6 +42,8 @@ object Deps {
|
||||||
const val tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
|
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 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 rxKotlin = "io.reactivex.rxjava2:rxkotlin:${Versions.rxKotlin}"
|
||||||
const val rxAndroid = "io.reactivex.rxjava2:rxandroid:${Versions.rxAndroid}"
|
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 = "com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}"
|
||||||
const val leakcanary_noop = "com.squareup.leakcanary:leakcanary-android-no-op:${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_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}"
|
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 = "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 = "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 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}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue