Skip to content

Commit

Permalink
Add unit tests for component module (#16)
Browse files Browse the repository at this point in the history
* add unit test for Flow.toValue()

* add unit test for LifecycleCoroutineScope

* add test module for decompose testing && write component tests

* add component view model test

* add run test on CI
  • Loading branch information
VladislavSumin authored Jan 3, 2025
1 parent fe0ff00 commit 73b08c0
Show file tree
Hide file tree
Showing 18 changed files with 2,526 additions and 5 deletions.
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()
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

0 comments on commit 73b08c0

Please sign in to comment.