Merge pull request #1182 from kiwix/macgills/#1174-introduce-unit-testing

Macgills/#1174 introduce unit testing
This commit is contained in:
macgills 2019-05-30 09:05:35 +01:00 committed by GitHub
commit fac411fe6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 666 additions and 61 deletions

View File

@ -25,11 +25,11 @@ We utilize different build variants (flavours) to build various different versio
- [OkHttp](https://github.com/square/okhttp) - An HTTP+SPDY client for Android and Java applications - [OkHttp](https://github.com/square/okhttp) - An HTTP+SPDY client for Android and Java applications
- [Butterknife](http://jakewharton.github.io/butterknife/) - View "injection" library for Android - [Butterknife](http://jakewharton.github.io/butterknife/) - View "injection" library for Android
- [Mockito](https://github.com/mockito/mockito) - Most popular Mocking framework for unit tests written in Java - [Mockito](https://github.com/mockito/mockito) - Most popular Mocking framework for unit tests written in Java
- [Guava](https://github.com/google/guava) - Collections, caching, primitives support, concurrency libraries, common annotations, string processing, I/O, and so forth.
- [Apache](https://github.com/apache/commons-io) - The Apache Commons IO library contains utility classes, stream implementations, file filters, file comparators, endian transformation classes, and much more.
- [RxJava](https://github.com/ReactiveX/RxJava) - Reactive Extensions for the JVM a library for composing asynchronous and event-based programs using observable sequences for the Java VM. - [RxJava](https://github.com/ReactiveX/RxJava) - Reactive Extensions for the JVM a library for composing asynchronous and event-based programs using observable sequences for the Java VM.
- [ObjectBox] (https://github.com/objectbox/objectbox-java) - Reactive NoSQL Databse to replace SquiDb
- [MockK] (https://github.com/mockk/mockk) - Kotlin mocking library that allows mocking of final classes by default.
- [JUnit5] (https://github.com/junit-team/junit5/) - The next generation of JUnit
- [AssertJ] (https://github.com/joel-costigliola/assertj-core) - Fluent assertions for test code
## Contributing ## Contributing

View File

@ -110,11 +110,9 @@ dependencies {
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
// JUnit // JUnit
testImplementation 'junit:junit:4.12'
androidTestImplementation 'junit:junit:4.12' androidTestImplementation 'junit:junit:4.12'
// Mockito // Mockito
testImplementation "org.mockito:mockito-core:2.24.5"
androidTestImplementation "org.mockito:mockito-android:2.24.5" androidTestImplementation "org.mockito:mockito-android:2.24.5"
// Leak canary // Leak canary
@ -136,6 +134,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 +370,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

@ -0,0 +1,13 @@
package org.kiwix.kiwixmobile.zim_manager
import android.content.Context
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.Language
import javax.inject.Inject
class DefaultLanguageProvider @Inject constructor(private val context: Context) {
fun provide() = Language(
context.resources.configuration.locale.isO3Language,
true,
1
)
}

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,9 @@ 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,
private val defaultLanguageProvider: DefaultLanguageProvider
) : ViewModel() { ) : ViewModel() {
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData() val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
@ -94,6 +98,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)
@ -105,16 +114,17 @@ class ZimManageViewModel @Inject constructor(
val downloadStatuses = downloadStatuses(downloads) val downloadStatuses = downloadStatuses(downloads)
val booksFromDao = books() val booksFromDao = books()
val networkLibrary = PublishProcessor.create<LibraryNetworkEntity>() val networkLibrary = PublishProcessor.create<LibraryNetworkEntity>()
val languages = languageDao.languages()
return arrayOf( return arrayOf(
updateDownloadItems(downloadStatuses), updateDownloadItems(downloadStatuses),
removeCompletedDownloadsFromDb(downloadStatuses), removeCompletedDownloadsFromDb(downloadStatuses),
removeNonExistingDownloadsFromDb(downloadStatuses, downloads), removeNonExistingDownloadsFromDb(downloadStatuses, downloads),
updateBookItems(booksFromDao), updateBookItems(booksFromDao),
checkFileSystemForBooksOnRequest(booksFromDao), checkFileSystemForBooksOnRequest(booksFromDao),
updateLibraryItems(booksFromDao, downloads, networkLibrary), updateLibraryItems(booksFromDao, downloads, networkLibrary, languages),
updateLanguagesInDao(networkLibrary), updateLanguagesInDao(networkLibrary, languages),
updateNetworkStates(), updateNetworkStates(),
updateLanguageItemsForDialog(), updateLanguageItemsForDialog(languages),
requestsAndConnectivtyChangesToLibraryRequests(networkLibrary) requestsAndConnectivtyChangesToLibraryRequests(networkLibrary)
) )
} }
@ -155,6 +165,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())
@ -184,8 +195,10 @@ class ZimManageViewModel @Inject constructor(
) )
} }
private fun updateLanguageItemsForDialog() = requestLanguagesDialog private fun updateLanguageItemsForDialog(languages: Flowable<List<Language>>) =
.withLatestFrom(languageDao.languages(), requestLanguagesDialog
.withLatestFrom(
languages,
BiFunction<Unit, List<Language>, List<Language>> { _, languages -> languages }) BiFunction<Unit, List<Language>, List<Language>> { _, languages -> languages })
.subscribe( .subscribe(
languageItems::postValue, languageItems::postValue,
@ -200,11 +213,12 @@ class ZimManageViewModel @Inject constructor(
private fun updateLibraryItems( private fun updateLibraryItems(
booksFromDao: Flowable<List<BookOnDisk>>, booksFromDao: Flowable<List<BookOnDisk>>,
downloads: Flowable<List<DownloadModel>>, downloads: Flowable<List<DownloadModel>>,
library: Flowable<LibraryNetworkEntity> library: Flowable<LibraryNetworkEntity>,
languages: Flowable<List<Language>>
) = Flowable.combineLatest( ) = Flowable.combineLatest(
booksFromDao, booksFromDao,
downloads, downloads,
languageDao.languages().filter { it.isNotEmpty() }, languages.filter { it.isNotEmpty() },
library, library,
requestFiltering requestFiltering
.doOnNext { libraryListIsRefreshing.postValue(true) } .doOnNext { libraryListIsRefreshing.postValue(true) }
@ -221,15 +235,17 @@ class ZimManageViewModel @Inject constructor(
) )
private fun updateLanguagesInDao( private fun updateLanguagesInDao(
library: Flowable<LibraryNetworkEntity> library: Flowable<LibraryNetworkEntity>,
languages: Flowable<List<Language>>
) = library ) = library
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.map { it.books } .map { it.books }
.withLatestFrom( .withLatestFrom(
languageDao.languages(), languages,
BiFunction(this::combineToLanguageList) BiFunction(this::combineToLanguageList)
) )
.map { it.sortedBy(Language::language) } .map { it.sortedBy(Language::language) }
.filter { it.isNotEmpty() }
.subscribe( .subscribe(
languageDao::insert, languageDao::insert,
Throwable::printStackTrace Throwable::printStackTrace
@ -278,11 +294,7 @@ class ZimManageViewModel @Inject constructor(
private fun defaultLanguage() = private fun defaultLanguage() =
listOf( listOf(
Language( defaultLanguageProvider.provide()
context.resources.configuration.locale.isO3Language,
true,
1
)
) )
private fun languageIsActive( private fun languageIsActive(
@ -411,7 +423,8 @@ 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

@ -18,7 +18,7 @@ class StorageObserver @Inject constructor(
private val downloadDao: NewDownloadDao private val downloadDao: NewDownloadDao
) { ) {
private val _booksOnFileSystem = PublishProcessor.create<Collection<BookOnDisk>>() private val _booksOnFileSystem = PublishProcessor.create<List<BookOnDisk>>()
val booksOnFileSystem = _booksOnFileSystem.distinctUntilChanged() val booksOnFileSystem = _booksOnFileSystem.distinctUntilChanged()
.doOnSubscribe { .doOnSubscribe {
downloadDao.downloads() downloadDao.downloads()
@ -37,7 +37,7 @@ class StorageObserver @Inject constructor(
} }
override fun onScanCompleted() { override fun onScanCompleted() {
_booksOnFileSystem.onNext(foundBooks) _booksOnFileSystem.onNext(foundBooks.toList())
} }
}).scan(sharedPreferenceUtil.prefStorage) }).scan(sharedPreferenceUtil.prefStorage)

View File

@ -2,22 +2,32 @@ package org.kiwix.kiwixmobile.zim_manager.library_view.adapter
import java.util.Locale import java.util.Locale
class Language constructor( data class Language constructor(
locale: Locale,
var active: Boolean, var active: Boolean,
var occurencesOfLanguage: Int, var occurencesOfLanguage: Int,
var language: String = locale.displayLanguage, var language: String,
var languageLocalized: String = locale.getDisplayLanguage(locale), var languageLocalized: String,
var languageCode: String = locale.isO3Language, var languageCode: String,
var languageCodeISO2: String = locale.language var languageCodeISO2: String
) { ) {
constructor(
locale: Locale,
active: Boolean,
occurrencesOfLanguage: Int
) : this(
active,
occurrencesOfLanguage,
locale.displayLanguage,
locale.getDisplayLanguage(locale),
locale.isO3Language,
locale.language
)
constructor( constructor(
languageCode: String, languageCode: String,
active: Boolean, active: Boolean,
occurrencesOfLanguage: Int occurrencesOfLanguage: Int
) : this(Locale(languageCode), active, occurrencesOfLanguage) { ) : this(Locale(languageCode), active, occurrencesOfLanguage)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return (other as Language).language == language && other.active == active return (other as Language).language == language && other.active == active

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,503 @@
/*
* 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.Single
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.R
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.library.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
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.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
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 org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
import java.io.File
import java.util.LinkedList
import java.util.concurrent.TimeUnit.MILLISECONDS
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 booksOnFileSystem: PublishProcessor<List<BookOnDisk>> = 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();
}
private val defaultLanguageProvider: DefaultLanguageProvider = mockk()
@BeforeEach
fun init() {
clearMocks(
newDownloadDao, newBookDao, newLanguagesDao, downloader,
storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils,
fat32Checker, uriToFileConverter, defaultLanguageProvider
)
every { connectivityBroadcastReceiver.action } returns "test"
every { newDownloadDao.downloads() } returns downloads
every { newBookDao.books() } returns books
every { storageObserver.booksOnFileSystem } returns booksOnFileSystem
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, defaultLanguageProvider
)
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()
expectStatusWith(listOf(expectedStatus))
viewModel.downloadItems
.test()
.assertValue(listOf(DownloadItem(expectedStatus)))
.dispose()
}
@Test
fun `on emission of successful status create a book and delete the download`() {
every { uriToFileConverter.convert(any()) } returns File("test")
val expectedStatus = DownloadStatus(
downloadId = 10L,
state = DownloadState.Successful
)
expectStatusWith(listOf(expectedStatus))
val element = expectedStatus.toBookOnDisk(uriToFileConverter)
verify {
newBookDao.insert(listOf(element))
newDownloadDao.delete(10L)
}
}
@Test
fun `if statuses don't have a matching Id for download in db over 3 secs then delete`() {
expectStatusWith(
listOf(DownloadStatus(downloadId = 1)),
listOf(DownloadModel(downloadId = 1), DownloadModel(downloadId = 3))
)
testScheduler.advanceTimeBy(3, SECONDS)
testScheduler.triggerActions()
verify {
newDownloadDao.delete(3)
}
}
@Test
fun `if statuses do have a matching Id for download in db over 3 secs then don't delete`() {
expectStatusWith(
listOf(DownloadStatus(downloadId = 1)),
listOf(DownloadModel(downloadId = 1))
)
testScheduler.advanceTimeBy(3, SECONDS)
testScheduler.triggerActions()
verify(exactly = 0) {
newDownloadDao.delete(any())
}
}
private fun expectStatusWith(
expectedStatuses: List<DownloadStatus>,
expectedDownloads: List<DownloadModel> = listOf(
DownloadModel()
)
) {
val downloadList = expectedDownloads
every { downloader.queryStatus(downloadList) } returns expectedStatuses
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
// @Disabled("WithLatestFrom is not calling BiFunction removeBooksAlreadyInDao - revisit")
fun `books found on filesystem are filtered by books already in db`() {
val expectedBook = BookOnDisk().apply { book.id = "1" }
val bookToRemove = BookOnDisk().apply { book.id = "2" }
testScheduler.triggerActions()
viewModel.requestFileSystemCheck.onNext(Unit)
testScheduler.triggerActions()
books.onNext(listOf(bookToRemove))
testScheduler.triggerActions()
booksOnFileSystem.onNext(
listOf(
expectedBook,
expectedBook,
bookToRemove
)
)
testScheduler.triggerActions()
verify {
newBookDao.insert(listOf(expectedBook))
}
}
}
@Nested
inner class Lanuages {
@Test
fun `a network request with no result and an empty language db triggers an activation of the default locale`() {
val expectedLanguage = Language(
active = true,
occurencesOfLanguage = 1,
language = "eng",
languageLocalized = "englocal",
languageCode = "ENG",
languageCodeISO2 = "en"
)
expectNetworkDbAndDefault(
listOf(),
listOf(),
expectedLanguage
)
verify { newLanguagesDao.insert(listOf(expectedLanguage)) }
}
@Test
fun `a network request with no result and a non empty language db does not trigger anything`() {
expectNetworkDbAndDefault(
listOf(),
listOf(
Language(
active = true,
occurencesOfLanguage = 1,
language = "eng",
languageLocalized = "englocal",
languageCode = "ENG",
languageCodeISO2 = "en"
)
),
Language(true, 1, "", "", "", "")
)
verify(exactly = 0) { newLanguagesDao.insert(any()) }
}
@Test
fun `a network request with a result and an empty language db triggers an activation of the default locale with the result of the web request`() {
val defaultLanguage = Language(
active = true,
occurencesOfLanguage = 1,
language = "English",
languageLocalized = "English",
languageCode = "eng",
languageCodeISO2 = "eng"
)
expectNetworkDbAndDefault(
listOf(
Book().apply { language = "eng" },
Book().apply { language = "eng" },
Book().apply { language = "fra" }
),
listOf(),
defaultLanguage)
verify {
newLanguagesDao.insert(
listOf(
defaultLanguage.copy(occurencesOfLanguage = 2),
Language(
active = false,
occurencesOfLanguage = 1,
language = "fra",
languageLocalized = "",
languageCode = "",
languageCodeISO2 = ""
)
)
)
}
}
@Test
fun `a network request with a result and a non empty language db triggers an activation of the result of the web request with the db`() {
val dbLanguage = Language(
active = true,
occurencesOfLanguage = 1,
language = "English",
languageLocalized = "English",
languageCode = "eng",
languageCodeISO2 = "eng"
)
expectNetworkDbAndDefault(
listOf(
Book().apply { language = "eng" },
Book().apply { language = "eng" },
Book().apply { language = "fra" }
),
listOf(dbLanguage),
Language(true, 1, "", "", "", "")
)
verify {
newLanguagesDao.insert(
listOf(
dbLanguage.copy(occurencesOfLanguage = 2),
Language(
active = false,
occurencesOfLanguage = 1,
language = "fra",
languageLocalized = "",
languageCode = "",
languageCodeISO2 = ""
)
)
)
}
}
private fun expectNetworkDbAndDefault(
networkBooks: List<Book>,
dbBooks: List<Language>,
defaultLanguage: Language
) {
every { kiwixService.library } returns Single.just(
LibraryNetworkEntity().apply {
book = LinkedList(networkBooks)
})
val defaultLanguage = defaultLanguage
every { defaultLanguageProvider.provide() } returns defaultLanguage
languages.onNext(dbBooks)
testScheduler.triggerActions()
networkStates.onNext(CONNECTED)
testScheduler.triggerActions()
}
}
@Test
fun `network states observed`() {
networkStates.offer(NOT_CONNECTED)
viewModel.networkStates.test()
.assertValue(NOT_CONNECTED)
.dispose()
}
@Test
fun `language items for dialog observed`() {
val expectedValue = listOf(Language(true, 1, "e", "e", "e", "e"))
testScheduler.triggerActions()
languages.onNext(expectedValue)
testScheduler.triggerActions()
viewModel.requestLanguagesDialog.onNext(Unit)
testScheduler.triggerActions()
viewModel.languageItems.test()
.assertValue(expectedValue)
.dispose()
}
@Test
fun `library update removes from sources`() {
every { application.getString(R.string.your_languages) } returns "1"
every { application.getString(R.string.other_languages) } returns "2"
val bookAlreadyOnDisk = Book().apply {
id = "0"
url = ""
}
val bookDownloading = Book().apply {
id = "1"
url = ""
}
val bookWithStackExchange = Book().apply {
id = "2"
url = "blahblah/stack_exchange/"
}
val bookWithActiveLanguage = Book().apply {
id = "3"
language = "activeLanguage"
url = ""
}
val bookWithInactiveLanguage = Book().apply {
id = "3"
language = "inactiveLanguage"
url = ""
}
every { kiwixService.library } returns Single.just(
LibraryNetworkEntity().apply {
book = LinkedList(
listOf(
bookAlreadyOnDisk,
bookDownloading,
bookWithStackExchange,
bookWithActiveLanguage,
bookWithInactiveLanguage
)
)
}
)
networkStates.onNext(CONNECTED)
downloads.onNext(listOf(DownloadModel(book = bookDownloading)))
books.onNext(listOf(BookOnDisk(book = bookAlreadyOnDisk)))
languages.onNext(
listOf(
Language(true, 1, "", "", "activeLanguage", ""),
Language(false, 1, "", "", "inactiveLanguage", "")
)
)
fileSystemStates.onNext(CanWrite4GbFile)
testScheduler.advanceTimeBy(500, MILLISECONDS)
testScheduler.triggerActions()
viewModel.libraryItems.test()
.assertValue(
listOf(
LibraryListItem.DividerItem(Long.MAX_VALUE, "1"),
LibraryListItem.BookItem(bookWithActiveLanguage),
LibraryListItem.DividerItem(Long.MIN_VALUE, "2"),
LibraryListItem.BookItem(bookWithInactiveLanguage)
)
)
.dispose()
}
@Test
fun `library filters out files over 4GB if file system state says to`() {
val bookOver4Gb = Book().apply {
id = "0"
url = ""
size = "${Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES + 1}"
}
every { kiwixService.library } returns Single.just(
LibraryNetworkEntity().apply {
book = LinkedList(listOf(bookOver4Gb))
}
)
networkStates.onNext(CONNECTED)
downloads.onNext(listOf())
books.onNext(listOf())
languages.onNext(listOf(Language(true, 1, "", "", "activeLanguage", "")))
fileSystemStates.onNext(CannotWrite4GbFile)
testScheduler.advanceTimeBy(500, MILLISECONDS)
testScheduler.triggerActions()
viewModel.libraryItems.test()
.assertValue(listOf())
.dispose()
}
}

View File

@ -0,0 +1 @@
relaxUnitFun=true

View File

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