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

@ -24,12 +24,12 @@ We utilize different build variants (flavours) to build various different versio
- [Retrofit](http://square.github.io/retrofit/) - Retrofit turns your REST API into a Java interface
- [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
- [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.
- [Mockito](https://github.com/mockito/mockito) - Most popular Mocking framework for unit tests written in Java
- [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

View File

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

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

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

View File

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