Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests for component module #16

Merged
merged 5 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
with:
gradle_command: detekt

check:
name: Check
unit-tests:
name: Unit tests
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
Expand All @@ -43,9 +43,30 @@ jobs:
- name: Gradle cache
uses: ./.github/actions/gradle_cache
with:
key: "check"
key: "unit-tests"

- name: Run gradle
uses: ./.github/actions/run_gradle
with:
gradle_command: :core:decompose:components:assemble
gradle_command: test allTests

assemble:
name: Assemble
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Clone repository
uses: actions/checkout@v4

- name: Setup java
uses: ./.github/actions/setup_java

- name: Gradle cache
uses: ./.github/actions/gradle_cache
with:
key: "assemble"

- name: Run gradle
uses: ./.github/actions/run_gradle
with:
gradle_command: assemble
1 change: 1 addition & 0 deletions build-scripts/common-settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Общая для проекта и build-scripts часть settings.gradle.kts
*/
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

dependencyResolutionManagement {
repositories {
Expand Down
4 changes: 4 additions & 0 deletions core/decompose/components/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ kotlin {
implementation(vsCoreLibs.kotlin.serialization.core)
implementation(vsCoreLibs.kotlin.serialization.json)
}

commonTest.dependencies {
implementation(projects.core.decompose.test)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public abstract class ViewModel {
private val viewModelScope = CoroutineScope(Dispatchers.Main.immediate)

@PublishedApi
internal val stateKeeper: StateKeeper = WhileConstructedViewModelStateKeeper!!
internal val stateKeeper: StateKeeper = let {
val keeper = WhileConstructedViewModelStateKeeper ?: throw WrongViewModelUsageException()
VladislavSumin marked this conversation as resolved.
Show resolved Hide resolved
WhileConstructedViewModelStateKeeper = null
keeper
}

/**
* Укороченная версия [stateIn] с использованием [viewModelScope] и [SharingStarted.Eagerly] по умолчанию.
Expand Down Expand Up @@ -95,3 +99,10 @@ public abstract class ViewModel {
viewModelScope.cancel()
}
}

internal class WrongViewModelUsageException : Exception(
"""Wrong ViewModel usage.
|ViewModel creation allowed only inside view model function in Component class.
|Only ONE view model can be create inside viewModel function at same time"""
.trimMargin(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package ru.vladislavsumin.core.decompose.components

import kotlinx.coroutines.test.runTest
import ru.vladislavsumin.core.decompose.test.BaseComponentTest
import ru.vladislavsumin.core.decompose.test.setMain
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotSame
import kotlin.test.assertSame

class ComponentTest : BaseComponentTest() {
@Test
fun testComponentRecreateFull() = runTest {
setMain()
val component = TestComponent(context)
val viewModel = component.testViewModel

recreateContext()

val component2 = TestComponent(context)
val viewModel2 = component2.testViewModel

assertNotSame(viewModel, viewModel2)
assertNotEquals(viewModel.testSaveableFlow.value, viewModel2.testSaveableFlow.value)
}

@Test
fun testComponentRecreateProcessDeath() = runTest {
setMain()
val component = TestComponent(context)
val viewModel = component.testViewModel

recreateContext(RecreateContextType.ProcessDeath)

val component2 = TestComponent(context)
val viewModel2 = component2.testViewModel

assertNotSame(viewModel, viewModel2)
assertEquals(viewModel.testSaveableFlow.value, viewModel2.testSaveableFlow.value)
}

@Test
fun testComponentRecreateConfigurationChange() = runTest {
setMain()
val component = TestComponent(context)
val viewModel = component.testViewModel

recreateContext(RecreateContextType.ConfigurationChange)

val component2 = TestComponent(context)
val viewModel2 = component2.testViewModel

assertSame(viewModel, viewModel2)
assertEquals(viewModel.testSaveableFlow.value, viewModel2.testSaveableFlow.value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.vladislavsumin.core.decompose.components

import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.test.runTest
import ru.vladislavsumin.core.decompose.test.BaseComponentTest
import ru.vladislavsumin.core.decompose.test.setMain
import kotlin.test.Test
import kotlin.test.assertFailsWith

class ComponentViewModelTest : BaseComponentTest() {
@Test
fun testComponentWrongDoubleViewModelCreation() = runTest {
setMain()

class TestComponent(context: ComponentContext) : Component(context) {
val viewModel = viewModel {
TestViewModel()
TestViewModel()
}
}

assertFailsWith(WrongViewModelUsageException::class) {
TestComponent(context)
}
}

@Test
fun testComponentWrongPlaceToCreateViewModel() = runTest {
setMain()

assertFailsWith(WrongViewModelUsageException::class) {
TestViewModel()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.vladislavsumin.core.decompose.components

import com.arkivanov.decompose.ComponentContext

class TestComponent(context: ComponentContext) : Component(context) {
val testViewModel = viewModel { TestViewModel() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.vladislavsumin.core.decompose.components

private var counter = 0

class TestViewModel : ViewModel() {
val testSaveableFlow = saveableStateFlow("KEY") { counter++ }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ru.vladislavsumin.core.decompose.components.utils

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

class FlowToValueTest {
@Test
fun testSimpleValue() = runTest {
val flow = MutableStateFlow(0)
val value = flow.asValue(this)
assertEquals(0, value.value)
}

@Test
fun testChangeSimpleValue() = runTest {
val flow = MutableStateFlow(0)
val value = flow.asValue(this)
flow.value = 2
assertEquals(2, value.value)
}

@Test
fun testSubscribeEmitCurrent() = runTest {
val flow = MutableStateFlow(0)
val value = flow.asValue(this)
var data = -1
val cancellation = value.subscribe { data = it }
runCurrent()
cancellation.cancel()
assertEquals(0, data)
}

@Test
fun testSubscribeEmitSequence() = runTest {
val flow = MutableStateFlow(0)
val value = flow.asValue(this)
var data = mutableListOf<Int>()
val cancellation = value.subscribe { data += it }
runCurrent()

flow.value = 1
runCurrent()

cancellation.cancel()

assertEquals(listOf(0, 1), data)
}

@Test
fun testSubscribeNotEmitAfterCancel() = runTest {
val flow = MutableStateFlow(0)
val value = flow.asValue(this)
var data = mutableListOf<Int>()
val cancellation = value.subscribe { data += it }
runCurrent()

cancellation.cancel()
flow.value = 1
runCurrent()

assertEquals(listOf(0), data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ru.vladislavsumin.core.decompose.components.utils

import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.essenty.lifecycle.create
import com.arkivanov.essenty.lifecycle.destroy
import kotlinx.coroutines.isActive
import kotlinx.coroutines.test.runTest
import ru.vladislavsumin.core.decompose.test.setMain
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class LifecycleCoroutineScopeTest {
@Test
fun testScopeCancellation() = runTest {
setMain()
val lifecycle = LifecycleRegistry()
lifecycle.create()

val scope = lifecycle.createCoroutineScope()
assertTrue(scope.isActive)

lifecycle.destroy()
assertFalse(scope.isActive)
}

@Test
fun testCreateScopeOnDestroyedLifecycle() = runTest {
setMain()
val lifecycle = LifecycleRegistry()
lifecycle.create()
lifecycle.destroy()

val scope = lifecycle.createCoroutineScope()
assertFalse(scope.isActive)
}
}
3 changes: 3 additions & 0 deletions core/decompose/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Decompose test

Набор утилит для написания тестов на compose компоненты
16 changes: 16 additions & 0 deletions core/decompose/test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
id("ru.vladislavsumin.convention.kmp.android-library")
id("ru.vladislavsumin.convention.kmp.js")
id("ru.vladislavsumin.convention.kmp.jvm")
}

kotlin {
sourceSets {
commonMain.dependencies {
implementation(kotlin("test"))
implementation(vsCoreLibs.decompose.core)
implementation(vsCoreLibs.kotlin.coroutines.core)
implementation(vsCoreLibs.kotlin.coroutines.test)
}
}
}
Loading