diff --git a/dbinspector/src/main/kotlin/com/infinum/dbinspector/data/sources/raw/AndroidDatabasesSource.kt b/dbinspector/src/main/kotlin/com/infinum/dbinspector/data/sources/raw/AndroidDatabasesSource.kt index a1d45bc7..3f67d70a 100644 --- a/dbinspector/src/main/kotlin/com/infinum/dbinspector/data/sources/raw/AndroidDatabasesSource.kt +++ b/dbinspector/src/main/kotlin/com/infinum/dbinspector/data/sources/raw/AndroidDatabasesSource.kt @@ -50,7 +50,7 @@ internal class AndroidDatabasesSource : Sources.Raw { try { val importedDatabases: MutableSet = mutableSetOf() - operation.importUris.forEach { uri -> + for (uri in operation.importUris) { uri.lastPathSegment?.split("/")?.last()?.let { filename -> operation.context.contentResolver.openInputStream(uri)?.use { inputStream -> val file = File(operation.context.databaseDir, filename) diff --git a/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/settings/SettingsActivity.kt b/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/settings/SettingsActivity.kt index 1a4838f0..fa012483 100644 --- a/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/settings/SettingsActivity.kt +++ b/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/settings/SettingsActivity.kt @@ -70,13 +70,13 @@ internal class SettingsActivity : BaseActivity() { private fun setupIgnoredTableNames(settings: Settings) = with(binding) { tableNameInputLayout.setEndIconOnClickListener { - tableNameInputLayout.editText?.text?.toString().orEmpty().trim().split(",").forEach { newName -> + for (newName in tableNameInputLayout.editText?.text?.toString().orEmpty().trim().split(",")) { addIgnoredTableNameView(newName.trim()) } } - settings.ignoredTableNames.forEach { + for (tableName in settings.ignoredTableNames) { namesLayout.addView( - createIgnoredTableNameView(it) + createIgnoredTableNameView(tableName) ) } } diff --git a/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/KeywordAdapter.kt b/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/KeywordAdapter.kt index 38aff884..37218bad 100644 --- a/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/KeywordAdapter.kt +++ b/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/KeywordAdapter.kt @@ -44,9 +44,9 @@ internal class KeywordAdapter( private fun createSpannable(keyword: Keyword) = SpannableString(keyword.value).apply { - spanFactory.findSpans(keyword.type).forEach { + for (span in spanFactory.findSpans(keyword.type)) { setSpan( - it, + span, 0, keyword.value.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE diff --git a/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/WordTokenizer.kt b/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/WordTokenizer.kt index bea5c74a..c094e9be 100644 --- a/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/WordTokenizer.kt +++ b/dbinspector/src/main/kotlin/com/infinum/dbinspector/ui/shared/views/editor/WordTokenizer.kt @@ -83,9 +83,9 @@ internal class WordTokenizer( spans: List, chopLastChar: Boolean = false ) = SpannableString(text).apply { - spans.forEach { + for (span in spans) { setSpan( - it, + span, 0, text.length - (if (chopLastChar) 1 else 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/content/shared/ContentViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/content/shared/ContentViewModelTest.kt index 89f472d0..87be65e5 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/content/shared/ContentViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/content/shared/ContentViewModelTest.kt @@ -15,7 +15,9 @@ import io.mockk.every import io.mockk.mockk import kotlin.test.assertEquals import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.DisplayName @@ -85,182 +87,163 @@ internal class ContentViewModelTest : BaseTest() { } @Test - fun `Content header is invoked`() { - test { - val useCase: BaseUseCase = get(qualifier = StringQualifier("schemaInfo")) - val viewModel = object : ContentViewModel( - get(), - get(), - useCase, - get(qualifier = StringQualifier("getSchema")), - get(qualifier = StringQualifier("dropSchema")) - ) { - override fun headerStatement(name: String): String = "" - - override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" - - override fun dropStatement(name: String): String = "" - }.apply { - databasePath = "test.db" - } - - coEvery { useCase.invoke(any()) } returns mockk { - every { cells } returns listOf( - mockk { - every { text } returns "my_column" - } - ) - } - - viewModel.header("my_content") - - coVerify(exactly = 1) { useCase.invoke(any()) } - - viewModel.stateFlow.test { - val item: ContentState? = awaitItem() - assertTrue(item is ContentState.Headers) - assertTrue(item.headers.isNotEmpty()) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } + fun `Content header is invoked`() = test { + val useCase: BaseUseCase = get(qualifier = StringQualifier("schemaInfo")) + val viewModel = object : ContentViewModel( + get(), + get(), + useCase, + get(qualifier = StringQualifier("getSchema")), + get(qualifier = StringQualifier("dropSchema")) + ) { + override fun headerStatement(name: String): String = "" + + override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" + + override fun dropStatement(name: String): String = "" + }.apply { + databasePath = "test.db" + } + + coEvery { useCase.invoke(any()) } returns mockk { + every { cells } returns listOf( + mockk { + every { text } returns "my_column" + } + ) } + + viewModel.header("my_content") + advanceUntilIdle() + + coVerify(exactly = 1) { useCase.invoke(any()) } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is ContentState.Headers) + assertTrue(state.headers.isNotEmpty()) + + assertNull(viewModel.errorFlow.value) } @Test - fun `Content data has cells`() { - test { - val useCase: BaseUseCase = get(qualifier = StringQualifier("getSchema")) - val viewModel = object : ContentViewModel( - get(), - get(), - get(qualifier = StringQualifier("schemaInfo")), - useCase, - get(qualifier = StringQualifier("dropSchema")) - ) { - override fun headerStatement(name: String): String = "" - - override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" - - override fun dropStatement(name: String): String = "" - }.apply { - databasePath = "test.db" - } - - coEvery { useCase.invoke(any()) } returns mockk { - every { cells } returns listOf(mockk()) - } - - viewModel.query("my_content", null, Sort.ASCENDING) - - coVerify(exactly = 0) { useCase.invoke(any()) } - - viewModel.stateFlow.test { - assertNull(awaitItem()) - val item: ContentState? = awaitItem() - assertTrue(item is ContentState.Content) - assertNotNull(item.content) - expectNoEvents() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - expectNoEvents() - } + fun `Content data has cells`() = test { + val useCase: BaseUseCase = get(qualifier = StringQualifier("getSchema")) + val viewModel = object : ContentViewModel( + get(), + get(), + get(qualifier = StringQualifier("schemaInfo")), + useCase, + get(qualifier = StringQualifier("dropSchema")) + ) { + override fun headerStatement(name: String): String = "" + + override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" + + override fun dropStatement(name: String): String = "" + }.apply { + databasePath = "test.db" + } + + coEvery { useCase.invoke(any()) } returns mockk { + every { cells } returns listOf(mockk()) + } + + viewModel.query("my_content", null, Sort.ASCENDING) + + coVerify(exactly = 0) { useCase.invoke(any()) } + + viewModel.stateFlow.test { + assertNull(awaitItem()) + val item: ContentState? = awaitItem() + assertTrue(item is ContentState.Content) + assertNotNull(item.content) + expectNoEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + expectNoEvents() } } @Test - fun `Drop content successful`() { - test { - val useCase: BaseUseCase = get(qualifier = StringQualifier("dropSchema")) - val viewModel = object : ContentViewModel( - get(), - get(), - get(qualifier = StringQualifier("schemaInfo")), - get(qualifier = StringQualifier("getSchema")), - useCase - ) { - override fun headerStatement(name: String): String = "" - - override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" - - override fun dropStatement(name: String): String = "" - }.apply { - databasePath = "test.db" - } - - coEvery { useCase.invoke(any()) } returns mockk { - every { cells } returns listOf() - } - - viewModel.drop("my_content") - - coVerify(exactly = 1) { useCase.invoke(any()) } - - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - val item: ContentEvent? = awaitItem() - assertTrue(item is ContentEvent.Dropped) - awaitCancellation() - } - viewModel.errorFlow.test { - expectNoEvents() - } + fun `Drop content successful`() = test { + val useCase: BaseUseCase = get(qualifier = StringQualifier("dropSchema")) + val viewModel = object : ContentViewModel( + get(), + get(), + get(qualifier = StringQualifier("schemaInfo")), + get(qualifier = StringQualifier("getSchema")), + useCase + ) { + override fun headerStatement(name: String): String = "" + + override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" + + override fun dropStatement(name: String): String = "" + }.apply { + databasePath = "test.db" + } + + coEvery { useCase.invoke(any()) } returns mockk { + every { cells } returns listOf() + } + + viewModel.drop("my_content") + advanceUntilIdle() + + coVerify(exactly = 1) { useCase.invoke(any()) } + + viewModel.stateFlow.test { + assertNull(awaitItem()) + } + viewModel.eventFlow.test { + val item: ContentEvent? = awaitItem() + assertTrue(item is ContentEvent.Dropped) + cancelAndIgnoreRemainingEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @Test - fun `Drop content failed`() { - test { - val useCase: BaseUseCase = get(qualifier = StringQualifier("dropSchema")) - val viewModel = object : ContentViewModel( - get(), - get(), - get(qualifier = StringQualifier("schemaInfo")), - get(qualifier = StringQualifier("getSchema")), - useCase - ) { - override fun headerStatement(name: String): String = "" - - override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" - - override fun dropStatement(name: String): String = "" - }.apply { - databasePath = "test.db" - } - - coEvery { useCase.invoke(any()) } returns mockk { - every { cells } returns listOf(mockk()) - } - - viewModel.drop("my_content") - - coVerify(exactly = 1) { useCase.invoke(any()) } - - viewModel.stateFlow.test { - assertNull(awaitItem()) - expectNoEvents() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - val item: Throwable? = awaitItem() - assertTrue(item is DropException) - assertNotNull(item.message) - assertEquals("Cannot perform a drop on selected schema.", item.message) - assertTrue(item.stackTrace.isNotEmpty()) - expectNoEvents() - } + fun `Drop content failed`() = test { + val useCase: BaseUseCase = get(qualifier = StringQualifier("dropSchema")) + val viewModel = object : ContentViewModel( + get(), + get(), + get(qualifier = StringQualifier("schemaInfo")), + get(qualifier = StringQualifier("getSchema")), + useCase + ) { + override fun headerStatement(name: String): String = "" + + override fun schemaStatement(name: String, orderBy: String?, sort: Sort): String = "" + + override fun dropStatement(name: String): String = "" + }.apply { + databasePath = "test.db" + } + + coEvery { useCase.invoke(any()) } returns mockk { + every { cells } returns listOf(mockk()) } + + viewModel.drop("my_content") + advanceUntilIdle() + + coVerify(exactly = 1) { useCase.invoke(any()) } + + assertNull(viewModel.stateFlow.value) + + val error = viewModel.errorFlow.filterNotNull().first() + assertTrue(error is DropException) + assertNotNull(error.message) + assertEquals("Cannot perform a drop on selected schema.", error.message) + assertTrue(error.stackTrace.isNotEmpty()) } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/DatabaseViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/DatabaseViewModelTest.kt index 478830c6..6d481c21 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/DatabaseViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/DatabaseViewModelTest.kt @@ -1,7 +1,6 @@ package com.infinum.dbinspector.ui.databases import android.content.Context -import app.cash.turbine.test import com.infinum.dbinspector.domain.UseCases import com.infinum.dbinspector.shared.BaseTest import io.mockk.coEvery @@ -10,7 +9,14 @@ import io.mockk.every import io.mockk.mockk import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.koin.core.module.Module @@ -31,7 +37,7 @@ internal class DatabaseViewModelTest : BaseTest() { ) @Test - fun `Browse and collect all databases`() { + fun `Browse and collect all databases`() = test { val useCase: UseCases.GetDatabases = get() val viewModel = DatabaseViewModel( useCase, @@ -46,29 +52,22 @@ internal class DatabaseViewModelTest : BaseTest() { ) viewModel.browse(get()) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 3) - assertTrue(item.databases[0].name == "blog") - assertTrue(item.databases[1].name == "chinook") - assertTrue(item.databases[2].name == "northwind") - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.count() == 3) + assertTrue(state.databases[0].name == "blog") + assertTrue(state.databases[1].name == "chinook") + assertTrue(state.databases[2].name == "northwind") + + assertNull(viewModel.errorFlow.value) } @Test - fun `Search database by name with result found`() { + fun `Search database by name with result found`() = test { val useCase: UseCases.GetDatabases = get() val viewModel = DatabaseViewModel( useCase, @@ -81,58 +80,52 @@ internal class DatabaseViewModelTest : BaseTest() { ) viewModel.browse(get(), "log") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 1) - assertTrue(item.databases.first().name == "blog") - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.count() == 1) + assertTrue(state.databases.first().name == "blog") + + assertNull(viewModel.errorFlow.value) } @Test fun `Search database by name without result found`() { - val useCase: UseCases.GetDatabases = get() - val viewModel = DatabaseViewModel( - useCase, - get(), - get() - ) + // Use UnconfinedTestDispatcher to ensure dispatcher is properly initialized + val testDispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(testDispatcher) - coEvery { useCase.invoke(any()) } returns listOf() + try { + val useCase: UseCases.GetDatabases = get() + val viewModel = DatabaseViewModel( + useCase, + get(), + get() + ) - viewModel.browse(get(), "south") + coEvery { useCase.invoke(any()) } returns listOf() - coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 0) - assertTrue(item.databases.isEmpty()) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() + viewModel.browse(get(), "south") + + coVerify(exactly = 1) { useCase.invoke(any()) } + + blockingTest { + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.isEmpty()) + + assertNull(viewModel.errorFlow.value) } + } finally { + Dispatchers.resetMain() } } @Test - fun `Import empty list of databases`() { + fun `Import empty list of databases`() = test { val getUseCase: UseCases.GetDatabases = get() val importUseCase: UseCases.ImportDatabases = get() @@ -148,28 +141,21 @@ internal class DatabaseViewModelTest : BaseTest() { coEvery { importUseCase.invoke(any()) } returns listOf() viewModel.import(get(), mockk()) + advanceUntilIdle() coVerify(exactly = 1) { importUseCase.invoke(any()) } coVerify(exactly = 1) { getUseCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 1) - assertTrue(item.databases.first().name == "blog") - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.count() == 1) + assertTrue(state.databases.first().name == "blog") + + assertNull(viewModel.errorFlow.value) } @Test - fun `Import a single database`() { + fun `Import a single database`() = test { val getUseCase: UseCases.GetDatabases = get() val importUseCase: UseCases.ImportDatabases = get() val viewModel = DatabaseViewModel( @@ -186,28 +172,21 @@ internal class DatabaseViewModelTest : BaseTest() { ) viewModel.import(get(), mockk()) + advanceUntilIdle() coVerify(exactly = 1) { importUseCase.invoke(any()) } coVerify(exactly = 1) { getUseCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 1) - assertTrue(item.databases.first().name == "blog") - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.count() == 1) + assertTrue(state.databases.first().name == "blog") + + assertNull(viewModel.errorFlow.value) } @Test - fun `Import multiple databases`() { + fun `Import multiple databases`() = test { val getUseCase: UseCases.GetDatabases = get() val importUseCase: UseCases.ImportDatabases = get() val viewModel = DatabaseViewModel( @@ -227,26 +206,19 @@ internal class DatabaseViewModelTest : BaseTest() { ) viewModel.import(get(), mockk()) + advanceUntilIdle() coVerify(exactly = 1) { importUseCase.invoke(any()) } coVerify(exactly = 1) { getUseCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 3) - assertTrue(item.databases[0].name == "blog") - assertTrue(item.databases[1].name == "chinook") - assertTrue(item.databases[2].name == "northwind") - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.count() == 3) + assertTrue(state.databases[0].name == "blog") + assertTrue(state.databases[1].name == "chinook") + assertTrue(state.databases[2].name == "northwind") + + assertNull(viewModel.errorFlow.value) } // @Test @@ -266,73 +238,77 @@ internal class DatabaseViewModelTest : BaseTest() { @Test fun `Copy database successful`() { - val getUseCase: UseCases.GetDatabases = get() - val copyUseCase: UseCases.CopyDatabase = get() - val viewModel = DatabaseViewModel( - getUseCase, - get(), - copyUseCase - ) - - coEvery { getUseCase.invoke(any()) } returns listOf( - mockk { every { name } returns "blog" }, - mockk { every { name } returns "blog_1" } - ) - coEvery { copyUseCase.invoke(any()) } returns listOf( - mockk { every { name } returns "blog_1" } - ) - - viewModel.copy(get(), mockk()) - - coVerify(exactly = 1) { copyUseCase.invoke(any()) } - coVerify(exactly = 1) { getUseCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: DatabaseState? = awaitItem() - assertTrue(item is DatabaseState.Databases) - assertTrue(item.databases.count() == 2) - assertTrue(item.databases[0].name == "blog") - assertTrue(item.databases[1].name == "blog_1") - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() + // Use UnconfinedTestDispatcher for this test to handle nested launches + val testDispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(testDispatcher) + + try { + val getUseCase: UseCases.GetDatabases = get() + val copyUseCase: UseCases.CopyDatabase = get() + val viewModel = DatabaseViewModel( + getUseCase, + get(), + copyUseCase + ) + + coEvery { getUseCase.invoke(any()) } returns listOf( + mockk { every { name } returns "blog" }, + mockk { every { name } returns "blog_1" } + ) + coEvery { copyUseCase.invoke(any()) } returns listOf( + mockk { every { name } returns "blog_1" } + ) + + viewModel.copy(get(), mockk()) + + coVerify(exactly = 1) { copyUseCase.invoke(any()) } + coVerify(exactly = 1) { getUseCase.invoke(any()) } + + blockingTest { + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is DatabaseState.Databases) + assertTrue(state.databases.count() == 2) + assertTrue(state.databases[0].name == "blog") + assertTrue(state.databases[1].name == "blog_1") + + assertNull(viewModel.errorFlow.value) } + } finally { + Dispatchers.resetMain() } } @Test fun `Copy database failed`() { - val getUseCase: UseCases.GetDatabases = get() - val copyUseCase: UseCases.CopyDatabase = get() - val viewModel = DatabaseViewModel( - getUseCase, - get(), - copyUseCase - ) + // Use UnconfinedTestDispatcher for this test to handle nested launches + val testDispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(testDispatcher) - coEvery { copyUseCase.invoke(any()) } returns listOf() + try { + val getUseCase: UseCases.GetDatabases = get() + val copyUseCase: UseCases.CopyDatabase = get() + val viewModel = DatabaseViewModel( + getUseCase, + get(), + copyUseCase + ) - viewModel.copy(get(), mockk()) + coEvery { copyUseCase.invoke(any()) } returns listOf() - coVerify(exactly = 1) { copyUseCase.invoke(any()) } - coVerify(exactly = 0) { getUseCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - val item: Throwable? = awaitItem() - assertTrue(item is Throwable) - assertNull(item.message) - assertTrue(item.stackTrace.isNotEmpty()) + viewModel.copy(get(), mockk()) + + coVerify(exactly = 1) { copyUseCase.invoke(any()) } + coVerify(exactly = 0) { getUseCase.invoke(any()) } + + assertNull(viewModel.stateFlow.value) + + blockingTest { + val error = viewModel.errorFlow.filterNotNull().first() + assertNotNull(error.message) + assertTrue(error.stackTrace.isNotEmpty()) } + } finally { + Dispatchers.resetMain() } } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/remove/RemoveDatabaseViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/remove/RemoveDatabaseViewModelTest.kt index d8dfc9da..00796cf7 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/remove/RemoveDatabaseViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/remove/RemoveDatabaseViewModelTest.kt @@ -9,8 +9,11 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlin.test.assertFalse +import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.koin.core.module.Module @@ -28,7 +31,7 @@ internal class RemoveDatabaseViewModelTest : BaseTest() { ) @Test - fun `Remove database successful`() { + fun `Remove database successful`() = test { val useCase: UseCases.RemoveDatabase = get() val viewModel = RemoveDatabaseViewModel( useCase @@ -42,26 +45,19 @@ internal class RemoveDatabaseViewModelTest : BaseTest() { get(), mockk() ) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: RemoveDatabaseState? = awaitItem() - assertTrue(item is RemoveDatabaseState.Removed) - assertTrue(item.success) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is RemoveDatabaseState.Removed) + assertTrue(state.success) + + assertNull(viewModel.errorFlow.value) } @Test - fun `Remove database failed`() { + fun `Remove database failed`() = test { val useCase: UseCases.RemoveDatabase = get() val viewModel = RemoveDatabaseViewModel( useCase @@ -73,21 +69,14 @@ internal class RemoveDatabaseViewModelTest : BaseTest() { get(), mockk() ) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: RemoveDatabaseState? = awaitItem() - assertTrue(item is RemoveDatabaseState.Removed) - assertFalse(item.success) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is RemoveDatabaseState.Removed) + assertFalse(state.success) + + assertNull(viewModel.errorFlow.value) } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/rename/RenameDatabaseViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/rename/RenameDatabaseViewModelTest.kt index 55ac9609..299535f1 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/rename/RenameDatabaseViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/databases/rename/RenameDatabaseViewModelTest.kt @@ -10,7 +10,10 @@ import io.mockk.every import io.mockk.mockk import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.koin.core.module.Module @@ -28,7 +31,7 @@ internal class RenameDatabaseViewModelTest : BaseTest() { ) @Test - fun `Rename database successful`() { + fun `Rename database successful`() = test { val useCase: UseCases.RenameDatabase = get() val viewModel = RenameDatabaseViewModel( useCase @@ -43,26 +46,19 @@ internal class RenameDatabaseViewModelTest : BaseTest() { mockk(), "invoices" ) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: RenameDatabaseState? = awaitItem() - assertTrue(item is RenameDatabaseState.Renamed) - assertTrue(item.success) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is RenameDatabaseState.Renamed) + assertTrue(state.success) + + assertNull(viewModel.errorFlow.value) } @Test - fun `Rename database failed`() { + fun `Rename database failed`() = test { val useCase: UseCases.RenameDatabase = get() val viewModel = RenameDatabaseViewModel( useCase @@ -75,21 +71,14 @@ internal class RenameDatabaseViewModelTest : BaseTest() { mockk(), "invoices" ) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - val item: Throwable? = awaitItem() - assertTrue(item is Throwable) - assertNull(item.message) - assertTrue(item.stackTrace.isNotEmpty()) - } - } + + assertNull(viewModel.stateFlow.value) + + val error = viewModel.errorFlow.filterNotNull().first() + assertNotNull(error.message) + assertTrue(error.stackTrace.isNotEmpty()) } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/EditViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/EditViewModelTest.kt index ff64dd7b..de932c5b 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/EditViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/EditViewModelTest.kt @@ -9,8 +9,10 @@ import io.mockk.every import io.mockk.mockk import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.DisplayName @@ -82,7 +84,7 @@ internal class EditViewModelTest : BaseTest() { } @Test - fun `Raw query content header is invoked`() { + fun `Raw query content header is invoked`() = test { val useCase: UseCases.GetRawQueryHeaders = get() val viewModel = EditViewModel( get(), @@ -108,26 +110,19 @@ internal class EditViewModelTest : BaseTest() { } viewModel.header("SELECT * FROM my_table") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: EditState? = awaitItem() - assertTrue(item is EditState.Headers) - assertTrue(item.headers.isNotEmpty()) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is EditState.Headers) + assertTrue(state.headers.isNotEmpty()) + + assertNull(viewModel.errorFlow.value) } @Test - fun `Raw query content data has cells`() { + fun `Raw query content data has cells`() = test { val useCase: UseCases.GetRawQuery = get() val viewModel = EditViewModel( get(), @@ -147,26 +142,27 @@ internal class EditViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns mockk() viewModel.query("SELECT * FROM my_table") + advanceUntilIdle() coVerify(exactly = 0) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: EditState? = awaitItem() - assertTrue(item is EditState.Content) - assertNotNull(item.content) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } + + viewModel.stateFlow.test { + val item: EditState? = awaitItem() + assertTrue(item is EditState.Content) + assertNotNull(item.content) + expectNoEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + expectNoEvents() } } @Test - fun `Raw query content data with affected rows successful`() { + fun `Raw query content data with affected rows successful`() = test { val useCase: UseCases.GetAffectedRows = get() val viewModel = EditViewModel( get(), @@ -186,26 +182,27 @@ internal class EditViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns mockk() viewModel.query("SELECT * FROM my_table") + advanceUntilIdle() coVerify(exactly = 0) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: EditState? = awaitItem() - assertTrue(item is EditState.Content) - assertNotNull(item.content) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } + + viewModel.stateFlow.test { + val item: EditState? = awaitItem() + assertTrue(item is EditState.Content) + assertNotNull(item.content) + expectNoEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + expectNoEvents() } } @Test - fun `Collect database keywords like table and column names, view names and trigger names`() { + fun `Collect database keywords like table and column names, view names and trigger names`() = test { val getTablesUseCase: UseCases.GetTables = get() val getTableInfoUseCase: UseCases.GetTableInfo = get() val viewModel = EditViewModel( @@ -239,23 +236,21 @@ internal class EditViewModelTest : BaseTest() { } viewModel.keywords() + advanceUntilIdle() // coVerify(exactly = 3) { getTablesUseCase.invoke(any()) } // coVerify(exactly = 1) { getTableInfoUseCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - val item: EditEvent? = awaitItem() - assertTrue(item is EditEvent.Keywords) - assertNotNull(item.keywords) - awaitCancellation() - } - viewModel.errorFlow.test { - expectNoEvents() - } + + assertNull(viewModel.stateFlow.value) + + viewModel.eventFlow.test { + val item: EditEvent? = awaitItem() + assertTrue(item is EditEvent.Keywords) + assertNotNull(item.keywords) + cancelAndIgnoreRemainingEvents() } + + assertNull(viewModel.errorFlow.value) } @Test @@ -292,7 +287,7 @@ internal class EditViewModelTest : BaseTest() { val item: EditEvent? = awaitItem() assertTrue(item is EditEvent.History) assertNotNull(item.history) - awaitCancellation() + cancelAndIgnoreRemainingEvents() } viewModel.errorFlow.test { expectNoEvents() @@ -333,7 +328,7 @@ internal class EditViewModelTest : BaseTest() { val item: EditEvent? = awaitItem() assertTrue(item is EditEvent.SimilarExecution) assertNotNull(item.history) - awaitCancellation() + cancelAndIgnoreRemainingEvents() } viewModel.errorFlow.test { expectNoEvents() @@ -367,7 +362,7 @@ internal class EditViewModelTest : BaseTest() { } @Test - fun `Successful execution is saved`() { + fun `Successful execution is saved`() = test { val useCase: UseCases.SaveExecution = get() val viewModel = EditViewModel( get(), @@ -387,6 +382,7 @@ internal class EditViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns mockk() viewModel.saveSuccessfulExecution("SELECT * FROM my_table") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } } @@ -417,7 +413,7 @@ internal class EditViewModelTest : BaseTest() { } @Test - fun `Failed execution is saved`() { + fun `Failed execution is saved`() = test { val useCase: UseCases.SaveExecution = get() val viewModel = EditViewModel( get(), @@ -437,6 +433,7 @@ internal class EditViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns mockk() viewModel.saveFailedExecution("SELECT * FROM my_table") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/history/HistoryViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/history/HistoryViewModelTest.kt index 8905ae60..6d9f7d3d 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/history/HistoryViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/edit/history/HistoryViewModelTest.kt @@ -10,8 +10,8 @@ import io.mockk.every import io.mockk.mockk import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.DisplayName @@ -33,7 +33,7 @@ internal class HistoryViewModelTest : BaseTest() { @Test @Disabled("Unfinished coroutines during teardown.") - fun `Load history for database path`() { + fun `Load history for database path`() = test { val useCase: UseCases.GetHistory = get() val viewModel = HistoryViewModel( useCase, @@ -54,27 +54,28 @@ internal class HistoryViewModelTest : BaseTest() { } viewModel.history("test.db") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - val item: HistoryState? = awaitItem() - assertTrue(item is HistoryState.History) - assertNotNull(item.history) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } + + viewModel.stateFlow.test { + assertNull(awaitItem()) + val item: HistoryState? = awaitItem() + assertTrue(item is HistoryState.History) + assertNotNull(item.history) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @Test - fun `Clear history for database path`() { + fun `Clear history for database path`() = test { val useCase: UseCases.ClearHistory = get() val viewModel = HistoryViewModel( get(), @@ -85,23 +86,16 @@ internal class HistoryViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.clearHistory("test.db") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } - } + + assertNull(viewModel.stateFlow.value) + assertNull(viewModel.errorFlow.value) } @Test - fun `Remove execution from history`() { + fun `Remove execution from history`() = test { val useCase: UseCases.RemoveExecution = get() val viewModel = HistoryViewModel( get(), @@ -119,18 +113,11 @@ internal class HistoryViewModelTest : BaseTest() { every { isSuccessful } returns true } ) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } - } + + assertNull(viewModel.stateFlow.value) + assertNull(viewModel.errorFlow.value) } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/pragma/shared/PragmaSourceViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/pragma/shared/PragmaSourceViewModelTest.kt index 9c5aa06f..bb343515 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/pragma/shared/PragmaSourceViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/pragma/shared/PragmaSourceViewModelTest.kt @@ -9,8 +9,9 @@ import com.infinum.dbinspector.shared.BaseTest import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk +import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -64,7 +65,7 @@ internal class PragmaSourceViewModelTest : BaseTest() { } @Test - fun `Pragma query successful`() { + fun `Pragma query successful`() = test { val useCase: BaseUseCase = get() val viewModel = object : PragmaSourceViewModel( get(), @@ -79,21 +80,22 @@ internal class PragmaSourceViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns mockk() viewModel.query("my_statement") + advanceUntilIdle() coVerify(exactly = 0) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: PragmaState? = awaitItem() - assertTrue(item is PragmaState.Pragma) - assertNotNull(item.pragma) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } + + viewModel.stateFlow.test { + val item: PragmaState? = awaitItem() + assertTrue(item is PragmaState.Pragma) + assertNotNull(item.pragma) + expectNoEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + expectNoEvents() } } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/schema/shared/SchemaSourceViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/schema/shared/SchemaSourceViewModelTest.kt index 1ce3eb01..a9d41475 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/schema/shared/SchemaSourceViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/schema/shared/SchemaSourceViewModelTest.kt @@ -9,8 +9,9 @@ import com.infinum.dbinspector.shared.BaseTest import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk +import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -64,7 +65,7 @@ internal class SchemaSourceViewModelTest : BaseTest() { } @Test - fun `Schema query successful`() { + fun `Schema query successful`() = test { val useCase: BaseUseCase = get() val viewModel = object : SchemaSourceViewModel( get(), @@ -79,21 +80,22 @@ internal class SchemaSourceViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns mockk() viewModel.query(viewModel.databasePath, "my_statement") + advanceUntilIdle() coVerify(exactly = 0) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: SchemaState? = awaitItem() - assertTrue(item is SchemaState.Schema) - assertNotNull(item.schema) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } + + viewModel.stateFlow.test { + val item: SchemaState? = awaitItem() + assertTrue(item is SchemaState.Schema) + assertNotNull(item.schema) + expectNoEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + expectNoEvents() } } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/settings/SettingsViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/settings/SettingsViewModelTest.kt index caf32c7a..dac166c9 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/settings/SettingsViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/settings/SettingsViewModelTest.kt @@ -12,7 +12,9 @@ import io.mockk.mockk import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -39,7 +41,7 @@ internal class SettingsViewModelTest : BaseTest() { ) @Test - fun `Get current default settings`() { + fun `Get current default settings`() = test { val useCase: UseCases.GetSettings = get() val viewModel = SettingsViewModel( useCase, @@ -60,30 +62,24 @@ internal class SettingsViewModelTest : BaseTest() { } viewModel.load() + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - val item: SettingsState? = awaitItem() - assertTrue(item is SettingsState.Settings) - assertFalse(item.settings.linesLimitEnabled) - assertTrue(item.settings.linesCount == 100) - assertEquals(item.settings.truncateMode, TruncateMode.END) - assertEquals(item.settings.blobPreviewMode, BlobPreviewMode.PLACEHOLDER) - assertTrue(item.settings.ignoredTableNames.isEmpty()) - awaitCancellation() - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - expectNoEvents() - } - } + + val state = viewModel.stateFlow.filterNotNull().first() + assertTrue(state is SettingsState.Settings) + assertFalse(state.settings.linesLimitEnabled) + assertTrue(state.settings.linesCount == 100) + assertEquals(state.settings.truncateMode, TruncateMode.END) + assertEquals(state.settings.blobPreviewMode, BlobPreviewMode.PLACEHOLDER) + assertTrue(state.settings.ignoredTableNames.isEmpty()) + + // Event flow and error flow should have no events + assertNull(viewModel.errorFlow.value) } @Test - fun `Save ignored table name`() { + fun `Save ignored table name`() = test { val useCase: UseCases.SaveIgnoredTableName = get() val viewModel = SettingsViewModel( get(), @@ -98,26 +94,27 @@ internal class SettingsViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.saveIgnoredTableName("android_metadata") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - val item: SettingsEvent? = awaitItem() - assertTrue(item is SettingsEvent.AddIgnoredTable) - assertTrue(item.name == "android_metadata") - awaitCancellation() - } - viewModel.errorFlow.test { - expectNoEvents() - } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + val item: SettingsEvent? = awaitItem() + assertTrue(item is SettingsEvent.AddIgnoredTable) + assertTrue(item.name == "android_metadata") + cancelAndIgnoreRemainingEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @Test - fun `Remove ignored table name`() { + fun `Remove ignored table name`() = test { val useCase: UseCases.RemoveIgnoredTableName = get() val viewModel = SettingsViewModel( get(), @@ -132,26 +129,27 @@ internal class SettingsViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.removeIgnoredTableName("android_metadata") + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - val item: SettingsEvent? = awaitItem() - assertTrue(item is SettingsEvent.RemoveIgnoredTable) - assertTrue(item.name == "android_metadata") - awaitCancellation() - } - viewModel.errorFlow.test { - expectNoEvents() - } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + val item: SettingsEvent? = awaitItem() + assertTrue(item is SettingsEvent.RemoveIgnoredTable) + assertTrue(item.name == "android_metadata") + cancelAndIgnoreRemainingEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @Test - fun `Toggle ON lines limit`() { + fun `Toggle ON lines limit`() = test { val useCase: UseCases.ToggleLinesLimit = get() val viewModel = SettingsViewModel( get(), @@ -166,23 +164,24 @@ internal class SettingsViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.toggleLinesLimit(true) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @Test - fun `Toggle OFF lines limit`() { + fun `Toggle OFF lines limit`() = test { val useCase: UseCases.ToggleLinesLimit = get() val viewModel = SettingsViewModel( get(), @@ -197,23 +196,24 @@ internal class SettingsViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.toggleLinesLimit(false) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @Test - fun `Save lines limit count`() { + fun `Save lines limit count`() = test { val useCase: UseCases.SaveLinesCount = get() val viewModel = SettingsViewModel( get(), @@ -228,56 +228,58 @@ internal class SettingsViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.saveLinesCount(any()) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @ParameterizedTest @EnumSource(TruncateMode::class) - fun `Save truncate per mode`(truncateMode: TruncateMode) { - test { - val useCase: UseCases.SaveTruncateMode = get() - val viewModel = SettingsViewModel( - get(), - get(), - get(), - get(), - get(), - useCase, - get() - ) - - coEvery { useCase.invoke(any()) } returns Unit - - viewModel.saveTruncateMode(truncateMode) - - coVerify(exactly = 1) { useCase.invoke(any()) } - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } + fun `Save truncate per mode`(truncateMode: TruncateMode) = test { + val useCase: UseCases.SaveTruncateMode = get() + val viewModel = SettingsViewModel( + get(), + get(), + get(), + get(), + get(), + useCase, + get() + ) + + coEvery { useCase.invoke(any()) } returns Unit + + viewModel.saveTruncateMode(truncateMode) + advanceUntilIdle() + + coVerify(exactly = 1) { useCase.invoke(any()) } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } @ParameterizedTest @EnumSource(BlobPreviewMode::class) - fun `Save blob preview per mode`(blobPreviewMode: BlobPreviewMode) { + fun `Save blob preview per mode`(blobPreviewMode: BlobPreviewMode) = test { val useCase: UseCases.SaveBlobPreviewMode = get() val viewModel = SettingsViewModel( get(), @@ -292,18 +294,19 @@ internal class SettingsViewModelTest : BaseTest() { coEvery { useCase.invoke(any()) } returns Unit viewModel.saveBlobPreviewType(blobPreviewMode) + advanceUntilIdle() coVerify(exactly = 1) { useCase.invoke(any()) } - test { - viewModel.stateFlow.test { - assertNull(awaitItem()) - } - viewModel.eventFlow.test { - expectNoEvents() - } - viewModel.errorFlow.test { - assertNull(awaitItem()) - } + viewModel.stateFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + viewModel.eventFlow.test { + expectNoEvents() + } + viewModel.errorFlow.test { + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() } } } diff --git a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/shared/base/lifecycle/LifecycleViewModelTest.kt b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/shared/base/lifecycle/LifecycleViewModelTest.kt index 2227d11a..c7cb7dad 100644 --- a/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/shared/base/lifecycle/LifecycleViewModelTest.kt +++ b/dbinspector/src/test/kotlin/com/infinum/dbinspector/ui/shared/base/lifecycle/LifecycleViewModelTest.kt @@ -5,6 +5,7 @@ import com.infinum.dbinspector.shared.BaseTest import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk +import kotlinx.coroutines.test.advanceUntilIdle import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -23,56 +24,52 @@ internal class LifecycleViewModelTest : BaseTest() { ) @Test - fun `Can be instantiated`() { - test { - val viewModel = object : LifecycleViewModel( - get(), - get() - ) {}.apply { - databasePath = "test.db" - } - - assertNotNull(viewModel) + fun `Can be instantiated`() = test { + val viewModel = object : LifecycleViewModel( + get(), + get() + ) {}.apply { + databasePath = "test.db" } + + assertNotNull(viewModel) } @Test - fun `Open connection invoked`() { - test { - val useCase: UseCases.OpenConnection = get() + fun `Open connection invoked`() = test { + val useCase: UseCases.OpenConnection = get() - coEvery { useCase.invoke(any()) } returns Unit + coEvery { useCase.invoke(any()) } returns Unit - val viewModel = object : LifecycleViewModel( - useCase, - get() - ) {}.apply { - databasePath = "test.db" - } + val viewModel = object : LifecycleViewModel( + useCase, + get() + ) {}.apply { + databasePath = "test.db" + } - viewModel.open() + viewModel.open() + advanceUntilIdle() - coVerify(exactly = 1) { useCase.invoke(any()) } - } + coVerify(exactly = 1) { useCase.invoke(any()) } } @Test - fun `Close connection invoked`() { - test { - val useCase: UseCases.CloseConnection = get() + fun `Close connection invoked`() = test { + val useCase: UseCases.CloseConnection = get() - coEvery { useCase.invoke(any()) } returns Unit + coEvery { useCase.invoke(any()) } returns Unit - val viewModel = object : LifecycleViewModel( - get(), - useCase - ) {}.apply { - databasePath = "test.db" - } + val viewModel = object : LifecycleViewModel( + get(), + useCase + ) {}.apply { + databasePath = "test.db" + } - viewModel.close() + viewModel.close() + advanceUntilIdle() - coVerify(exactly = 1) { useCase.invoke(any()) } - } + coVerify(exactly = 1) { useCase.invoke(any()) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe0e50ea..b40f3f3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ dbinspector = "5.4.9" gradle = "7.3.1" kotlin = "1.7.20" -coroutines = "1.6.4" +coroutines = "1.7.3" core = "1.9.0" appcompat = "1.5.1" activity = "1.6.0" @@ -31,7 +31,7 @@ junit5 = "5.9.1" mockk = "1.13.2" mockito = "4.8.1" mockitokotlin = "4.0.0" -turbine = "0.12.0" +turbine = "1.0.0" [libraries] library = { module = "com.infinum.dbinspector:dbinspector", version.ref = "dbinspector" }