#1174 Introduce Unit Testing - example of unit testing of a viewmodel

This commit is contained in:
Sean Mac Gillicuddy 2019-05-27 17:02:04 +01:00
parent fcac33cf2d
commit 9e3bbe58ba
10 changed files with 309 additions and 25 deletions

View File

@ -136,6 +136,14 @@ dependencies {
implementation "android.arch.lifecycle:extensions:1.1.1" implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "io.objectbox:objectbox-kotlin:$objectboxVersion" implementation "io.objectbox:objectbox-kotlin:$objectboxVersion"
implementation "io.objectbox:objectbox-rxjava:$objectboxVersion" implementation "io.objectbox:objectbox-rxjava:$objectboxVersion"
testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
testImplementation "io.mockk:mockk:1.9"
testImplementation "org.assertj:assertj-core:3.11.1"
//update this with androidx
testImplementation 'com.jraska.livedata:testing-ktx:0.2.1'
testImplementation 'android.arch.core:core-testing:1.1.1'
} }
// Set custom app import directory // Set custom app import directory
@ -364,6 +372,9 @@ android {
androidExtensions { androidExtensions {
experimental = true experimental = true
} }
testOptions {
unitTests.returnDefaultValues = true
}
} }
// Testdroid deployment configuration // Testdroid deployment configuration

View File

@ -25,6 +25,7 @@ import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.android.AndroidInjectionModule; import dagger.android.AndroidInjectionModule;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter;
import org.kiwix.kiwixmobile.utils.BookUtils; import org.kiwix.kiwixmobile.utils.BookUtils;
@Module(includes = { @Module(includes = {
@ -52,4 +53,9 @@ public class ApplicationModule {
BookUtils provideBookUtils() { BookUtils provideBookUtils() {
return new BookUtils(); return new BookUtils();
} }
@Provides @Singleton
UriToFileConverter provideUriToFIleCOnverter() {
return new UriToFileConverter.Impl();
}
} }

View File

@ -6,8 +6,8 @@ import java.io.File
data class BookOnDisk( data class BookOnDisk(
val databaseId: Long? = null, val databaseId: Long? = null,
val book: Book, val book: Book = Book().apply { id = "" },
val file: File val file: File = File("")
) { ) {
constructor(bookOnDiskEntity: BookOnDiskEntity) : this( constructor(bookOnDiskEntity: BookOnDiskEntity) : this(
bookOnDiskEntity.id, bookOnDiskEntity.id,

View File

@ -17,12 +17,11 @@
*/ */
package org.kiwix.kiwixmobile.downloader.model package org.kiwix.kiwixmobile.downloader.model
import android.view.WindowId
import org.kiwix.kiwixmobile.database.newdb.entities.DownloadEntity
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
data class DownloadModel( data class DownloadModel(
val databaseId: Long? = null, val databaseId: Long? = null,
val downloadId: Long, val downloadId: Long = 0,
val book: LibraryNetworkEntity.Book val book: LibraryNetworkEntity.Book = Book().apply { id = "" }
) )

View File

@ -55,21 +55,22 @@ import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
import java.io.File import java.io.File
class DownloadStatus( class DownloadStatus(
val downloadId: Long, val downloadId: Long = 0L,
val title: String, val title: String = "",
val description: String, val description: String = "",
val state: DownloadState, val state: DownloadState = DownloadState.Pending,
val bytesDownloadedSoFar: Long, val bytesDownloadedSoFar: Long = 0,
val totalSizeBytes: Long, val totalSizeBytes: Long = 0,
val lastModified: String, val lastModified: String = "",
val localUri: String?, val localUri: String? = null,
val mediaProviderUri: String?, val mediaProviderUri: String? = null,
val mediaType: String?, val mediaType: String? = null,
val uri: String?, val uri: String? = null,
val book: Book val book: Book = Book().apply { id = "" }
) { ) {
fun toBookOnDisk() = BookOnDisk(book = book, file = File(Uri.parse(localUri).path)) fun toBookOnDisk(uriToFileConverter:UriToFileConverter) =
BookOnDisk(book = book, file = uriToFileConverter.convert(localUri))
constructor( constructor(
cursor: Cursor, cursor: Cursor,
@ -90,6 +91,11 @@ class DownloadStatus(
) )
} }
interface UriToFileConverter {
fun convert(uriString: String?) = File(Uri.parse(uriString).path)
class Impl:UriToFileConverter{}
}
sealed class DownloadState(val stringId: Int) { sealed class DownloadState(val stringId: Int) {
companion object { companion object {
fun from( fun from(

View File

@ -3,8 +3,8 @@ package org.kiwix.kiwixmobile.zim_manager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
import io.reactivex.Flowable
import io.reactivex.processors.BehaviorProcessor import io.reactivex.processors.BehaviorProcessor
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.extensions.networkState import org.kiwix.kiwixmobile.extensions.networkState
import javax.inject.Inject import javax.inject.Inject
@ -13,14 +13,14 @@ class ConnectivityBroadcastReceiver @Inject constructor(private val connectivity
override val action: String = ConnectivityManager.CONNECTIVITY_ACTION override val action: String = ConnectivityManager.CONNECTIVITY_ACTION
val networkStates = private val _networkStates = BehaviorProcessor.createDefault(connectivityManager.networkState)
BehaviorProcessor.createDefault(connectivityManager.networkState) val networkStates: Flowable<NetworkState> = _networkStates
override fun onIntentWithActionReceived( override fun onIntentWithActionReceived(
context: Context, context: Context,
intent: Intent intent: Intent
) { ) {
networkStates.onNext(connectivityManager.networkState) _networkStates.onNext(connectivityManager.networkState)
} }
} }

View File

@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.zim_manager
import android.app.Application import android.app.Application
import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel import android.arch.lifecycle.ViewModel
import android.support.annotation.VisibleForTesting
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -39,6 +40,7 @@ import org.kiwix.kiwixmobile.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.downloader.model.DownloadModel import org.kiwix.kiwixmobile.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Successful import org.kiwix.kiwixmobile.downloader.model.DownloadState.Successful
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter
import org.kiwix.kiwixmobile.extensions.calculateSearchMatches import org.kiwix.kiwixmobile.extensions.calculateSearchMatches
import org.kiwix.kiwixmobile.extensions.registerReceiver import org.kiwix.kiwixmobile.extensions.registerReceiver
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
@ -71,7 +73,8 @@ class ZimManageViewModel @Inject constructor(
private val context: Application, private val context: Application,
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver,
private val bookUtils: BookUtils, private val bookUtils: BookUtils,
private val fat32Checker: Fat32Checker private val fat32Checker: Fat32Checker,
private val uriToFileConverter: UriToFileConverter
) : ViewModel() { ) : ViewModel() {
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData() val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
@ -94,6 +97,11 @@ class ZimManageViewModel @Inject constructor(
context.registerReceiver(connectivityBroadcastReceiver) context.registerReceiver(connectivityBroadcastReceiver)
} }
@VisibleForTesting
fun onClearedExposed() {
onCleared()
}
override fun onCleared() { override fun onCleared() {
compositeDisposable.clear() compositeDisposable.clear()
context.unregisterReceiver(connectivityBroadcastReceiver) context.unregisterReceiver(connectivityBroadcastReceiver)
@ -155,6 +163,7 @@ class ZimManageViewModel @Inject constructor(
) )
.buffer(3, SECONDS) .buffer(3, SECONDS)
.map(this::downloadIdsWithNoStatusesOverBufferPeriod) .map(this::downloadIdsWithNoStatusesOverBufferPeriod)
.filter { it.isNotEmpty() }
.subscribe( .subscribe(
{ {
downloadDao.delete(*it.toLongArray()) downloadDao.delete(*it.toLongArray())
@ -411,7 +420,7 @@ class ZimManageViewModel @Inject constructor(
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.subscribe( .subscribe(
{ {
bookDao.insert(it.map { downloadStatus -> downloadStatus.toBookOnDisk() }) bookDao.insert(it.map { downloadStatus -> downloadStatus.toBookOnDisk(uriToFileConverter) })
downloadDao.delete( downloadDao.delete(
*it.map { status -> status.downloadId }.toLongArray() *it.map { status -> status.downloadId }.toLongArray()
) )

View File

@ -0,0 +1,44 @@
/*
* Kiwix Android
* Copyright (C) 2018 Kiwix <android.kiwix.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kiwix.kiwixmobile
import android.arch.core.executor.ArchTaskExecutor
import android.arch.core.executor.TaskExecutor
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance()
.setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}

View File

@ -0,0 +1,208 @@
/*
* Kiwix Android
* Copyright (C) 2018 Kiwix <android.kiwix.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kiwix.kiwixmobile.zim_manager
import android.app.Application
import com.jraska.livedata.test
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.reactivex.Scheduler
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.InstantExecutorExtension
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.downloader.Downloader
import org.kiwix.kiwixmobile.downloader.model.BookOnDisk
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.downloader.model.DownloadState
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter
import org.kiwix.kiwixmobile.network.KiwixService
import org.kiwix.kiwixmobile.utils.BookUtils
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState
import org.kiwix.kiwixmobile.zim_manager.NetworkState.NOT_CONNECTED
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.Language
import java.io.File
import java.util.concurrent.TimeUnit.SECONDS
@ExtendWith(InstantExecutorExtension::class)
class ZimManageViewModelTest {
private val newDownloadDao: NewDownloadDao = mockk()
private val newBookDao: NewBookDao = mockk()
private val newLanguagesDao: NewLanguagesDao = mockk()
private val downloader: Downloader = mockk()
private val storageObserver: StorageObserver = mockk()
private val kiwixService: KiwixService = mockk()
private val application: Application = mockk()
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver = mockk()
private val bookUtils: BookUtils = mockk()
private val fat32Checker: Fat32Checker = mockk()
private val uriToFileConverter: UriToFileConverter = mockk()
lateinit var viewModel: ZimManageViewModel
private val downloads: PublishProcessor<List<DownloadModel>> = PublishProcessor.create()
private val books: PublishProcessor<List<BookOnDisk>> = PublishProcessor.create()
private val languages: PublishProcessor<List<Language>> = PublishProcessor.create()
private val fileSystemStates: PublishProcessor<FileSystemState> = PublishProcessor.create()
private val networkStates: PublishProcessor<NetworkState> = PublishProcessor.create()
private val testScheduler = TestScheduler()
init {
setScheduler(testScheduler)
}
private fun setScheduler(replacementScheduler: Scheduler) {
RxJavaPlugins.setIoSchedulerHandler { scheduler -> replacementScheduler }
RxJavaPlugins.setComputationSchedulerHandler { scheduler -> replacementScheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler -> replacementScheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> Schedulers.trampoline() }
}
@AfterAll
fun teardown() {
RxJavaPlugins.reset()
RxAndroidPlugins.reset();
}
@BeforeEach
fun init() {
clearMocks(
newDownloadDao, newBookDao, newLanguagesDao, downloader,
storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils,
fat32Checker, uriToFileConverter
)
every { connectivityBroadcastReceiver.action } returns "test"
every { newDownloadDao.downloads() } returns downloads
every { newBookDao.books() } returns books
every { newLanguagesDao.languages() } returns languages
every { fat32Checker.fileSystemStates } returns fileSystemStates
every { connectivityBroadcastReceiver.networkStates } returns networkStates
every { application.registerReceiver(any(), any()) } returns mockk()
viewModel = ZimManageViewModel(
newDownloadDao, newBookDao, newLanguagesDao, downloader,
storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils,
fat32Checker, uriToFileConverter
)
testScheduler.triggerActions()
}
@Nested
inner class Context {
@Test
fun `registers broadcastReceiver in init`() {
verify {
application.registerReceiver(connectivityBroadcastReceiver, any())
}
}
@Test
fun `unregisters broadcastReceiver in onCleared`() {
every { application.unregisterReceiver(any()) } returns mockk()
viewModel.onClearedExposed()
verify {
application.unregisterReceiver(connectivityBroadcastReceiver)
}
}
}
@Nested
inner class Downloads {
@Test
fun `on emission from database query and render downloads`() {
val expectedStatus = DownloadStatus()
expectStatus(expectedStatus)
viewModel.downloadItems
.test()
.assertValue(listOf(DownloadItem(expectedStatus)))
.dispose()
}
@Test
fun `on emission of successful status create a book and delete the download`() {
every { newBookDao.insert(any()) } returns Unit
every { newDownloadDao.delete(any()) } returns Unit
every { uriToFileConverter.convert(any()) } returns File("test")
val expectedStatus = DownloadStatus(
downloadId = 10L,
state = DownloadState.Successful
)
expectStatus(expectedStatus)
val element = expectedStatus.toBookOnDisk(uriToFileConverter)
verify {
newBookDao.insert(listOf(element))
newDownloadDao.delete(10L)
}
}
private fun expectStatus(expectedStatus: DownloadStatus) {
val downloadList = listOf(DownloadModel())
every { downloader.queryStatus(downloadList) } returns listOf(expectedStatus)
downloads.offer(downloadList)
testScheduler.triggerActions()
testScheduler.advanceTimeBy(1, SECONDS)
testScheduler.triggerActions()
}
}
@Nested
inner class Books {
@Test
fun `emissions from DB sorted by title and observed`() {
books.onNext(
listOf(
BookOnDisk().apply { book.title = "z" },
BookOnDisk().apply { book.title = "a" }
)
)
testScheduler.triggerActions()
viewModel.bookItems.test()
.assertValue(
listOf(
BookOnDisk().apply { book.title = "a" },
BookOnDisk().apply { book.title = "z" }
)
)
.dispose()
}
}
@Test
fun `network states observed`() {
networkStates.offer(NOT_CONNECTED)
viewModel.networkStates.test()
.assertValue(NOT_CONNECTED)
.dispose()
}
}

View File

@ -0,0 +1 @@
junit.jupiter.testinstance.lifecycle.default = per_class