mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-21 19:41:38 -04:00
#1174 Introduce Unit Testing - example of unit testing of a viewmodel
This commit is contained in:
parent
fcac33cf2d
commit
9e3bbe58ba
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 = "" }
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
1
app/src/test/resources/junit-platform.properties
Normal file
1
app/src/test/resources/junit-platform.properties
Normal file
@ -0,0 +1 @@
|
||||
junit.jupiter.testinstance.lifecycle.default = per_class
|
Loading…
x
Reference in New Issue
Block a user