#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 "io.objectbox:objectbox-kotlin:$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
@ -364,6 +372,9 @@ android {
androidExtensions {
experimental = true
}
testOptions {
unitTests.returnDefaultValues = true
}
}
// Testdroid deployment configuration

View File

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

View File

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

View File

@ -17,12 +17,11 @@
*/
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.Book
data class DownloadModel(
val databaseId: Long? = null,
val downloadId: Long,
val book: LibraryNetworkEntity.Book
val downloadId: Long = 0,
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
class DownloadStatus(
val downloadId: Long,
val title: String,
val description: String,
val state: DownloadState,
val bytesDownloadedSoFar: Long,
val totalSizeBytes: Long,
val lastModified: String,
val localUri: String?,
val mediaProviderUri: String?,
val mediaType: String?,
val uri: String?,
val book: Book
val downloadId: Long = 0L,
val title: String = "",
val description: String = "",
val state: DownloadState = DownloadState.Pending,
val bytesDownloadedSoFar: Long = 0,
val totalSizeBytes: Long = 0,
val lastModified: String = "",
val localUri: String? = null,
val mediaProviderUri: String? = null,
val mediaType: String? = null,
val uri: String? = null,
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(
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) {
companion object {
fun from(

View File

@ -3,8 +3,8 @@ package org.kiwix.kiwixmobile.zim_manager
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import io.reactivex.Flowable
import io.reactivex.processors.BehaviorProcessor
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.extensions.networkState
import javax.inject.Inject
@ -13,14 +13,14 @@ class ConnectivityBroadcastReceiver @Inject constructor(private val connectivity
override val action: String = ConnectivityManager.CONNECTIVITY_ACTION
val networkStates =
BehaviorProcessor.createDefault(connectivityManager.networkState)
private val _networkStates = BehaviorProcessor.createDefault(connectivityManager.networkState)
val networkStates: Flowable<NetworkState> = _networkStates
override fun onIntentWithActionReceived(
context: Context,
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.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import android.support.annotation.VisibleForTesting
import io.reactivex.Flowable
import io.reactivex.disposables.CompositeDisposable
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.DownloadState.Successful
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.registerReceiver
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
@ -71,7 +73,8 @@ class ZimManageViewModel @Inject constructor(
private val context: Application,
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver,
private val bookUtils: BookUtils,
private val fat32Checker: Fat32Checker
private val fat32Checker: Fat32Checker,
private val uriToFileConverter: UriToFileConverter
) : ViewModel() {
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
@ -94,6 +97,11 @@ class ZimManageViewModel @Inject constructor(
context.registerReceiver(connectivityBroadcastReceiver)
}
@VisibleForTesting
fun onClearedExposed() {
onCleared()
}
override fun onCleared() {
compositeDisposable.clear()
context.unregisterReceiver(connectivityBroadcastReceiver)
@ -155,6 +163,7 @@ class ZimManageViewModel @Inject constructor(
)
.buffer(3, SECONDS)
.map(this::downloadIdsWithNoStatusesOverBufferPeriod)
.filter { it.isNotEmpty() }
.subscribe(
{
downloadDao.delete(*it.toLongArray())
@ -411,7 +420,7 @@ class ZimManageViewModel @Inject constructor(
.filter { it.isNotEmpty() }
.subscribe(
{
bookDao.insert(it.map { downloadStatus -> downloadStatus.toBookOnDisk() })
bookDao.insert(it.map { downloadStatus -> downloadStatus.toBookOnDisk(uriToFileConverter) })
downloadDao.delete(
*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