From 881b218e23fd38feb3f728bdba918c6381d3e9fe Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 6 Mar 2024 21:43:02 +0100 Subject: [PATCH] Added kotest extentions (#25) * Added kotest extentions * Added MVI kotest release workflow --- .github/workflows/release_kotest.yml | 49 ++++++++++++++ docs/README.md | 1 + mvi-kotest/build.gradle.kts | 20 ++++++ mvi-kotest/gradle.properties | 7 ++ .../kotest/BehaviorSpecRootScopeExtensions.kt | 27 ++++++++ .../mvi/kotest/ViewModelContainerScope.kt | 59 +++++++++++++++++ .../mvi/kotest/ViewModelWhenContainerScope.kt | 65 +++++++++++++++++++ settings.gradle.kts | 1 + 8 files changed, 229 insertions(+) create mode 100644 .github/workflows/release_kotest.yml create mode 100644 mvi-kotest/build.gradle.kts create mode 100644 mvi-kotest/gradle.properties create mode 100644 mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/BehaviorSpecRootScopeExtensions.kt create mode 100644 mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelContainerScope.kt create mode 100644 mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelWhenContainerScope.kt diff --git a/.github/workflows/release_kotest.yml b/.github/workflows/release_kotest.yml new file mode 100644 index 0000000..12bba74 --- /dev/null +++ b/.github/workflows/release_kotest.yml @@ -0,0 +1,49 @@ +# This is a basic workflow to help you get started with Actions + +name: Release MVI Kotest + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + tags: + - 'kotest/[0-9]+.[0-9]+.[0-9]+' + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + + - name: Grant Permission to Execute Gradle + run: chmod +x gradlew + + - name: Build with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: build + + - name: Publish Library + run: | + echo "Publishing library🚀" + ./gradlew :mvi-kotest:publish --no-daemon --no-parallel + echo "Published✅" + echo "Releasing repository...🚀" + ./gradlew closeAndReleaseRepository + echo "Released✅" + env: + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/docs/README.md b/docs/README.md index f07fe7d..30c00a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ Here are the current available mvi modules versions: |-------------|:----------------------------------------------------------------------------------------------------------------------------------------------------:| | mvi | [![Maven Central](https://img.shields.io/maven-central/v/com.adidas.mvi/mvi)](https://mvnrepository.com/artifact/com.adidas.mvi/mvi) | | mvi-compose | [![Maven Central](https://img.shields.io/maven-central/v/com.adidas.mvi/mvi-compose)](https://mvnrepository.com/artifact/com.adidas.mvi/mvi-compose) | +| mvi-kotest | [![Maven Central](https://img.shields.io/maven-central/v/com.adidas.mvi/mvi-kotest)](https://mvnrepository.com/artifact/com.adidas.mvi/mvi-kotest) | ## Features diff --git a/mvi-kotest/build.gradle.kts b/mvi-kotest/build.gradle.kts new file mode 100644 index 0000000..605f868 --- /dev/null +++ b/mvi-kotest/build.gradle.kts @@ -0,0 +1,20 @@ +import org.jlleitschuh.gradle.ktlint.KtlintExtension + +plugins { + kotlin("jvm") version libs.versions.kotlin.get() + alias(libs.plugins.ktlint) + alias(libs.plugins.mavenPublish) +} + +kotlin { + explicitApi() +} + +configure { + version.set(libs.versions.ktlint.lib.get()) +} + +dependencies { + implementation(libs.kotest.runner) + implementation(libs.adidas.mvi) +} diff --git a/mvi-kotest/gradle.properties b/mvi-kotest/gradle.properties new file mode 100644 index 0000000..02ab54f --- /dev/null +++ b/mvi-kotest/gradle.properties @@ -0,0 +1,7 @@ +POM_ARTIFACT_ID=mvi-kotest +GROUP=com.adidas.mvi +VERSION_CODE=1 +VERSION_NAME=0.0.1 +POM_NAME=Adidas MVI Kotest +POM_DESCRIPTION=Adidas MVI Kotest +POM_PACKAGING=aar diff --git a/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/BehaviorSpecRootScopeExtensions.kt b/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/BehaviorSpecRootScopeExtensions.kt new file mode 100644 index 0000000..459ff23 --- /dev/null +++ b/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/BehaviorSpecRootScopeExtensions.kt @@ -0,0 +1,27 @@ +package com.adidas.mvi.kotest + +import com.adidas.mvi.Intent +import com.adidas.mvi.LoggableState +import com.adidas.mvi.MviHost +import com.adidas.mvi.State +import io.kotest.core.names.TestName +import io.kotest.core.spec.style.scopes.BehaviorSpecRootScope +import io.kotest.core.spec.style.scopes.addContainer + +@Suppress("FunctionName") +public inline fun < + TIntent : Intent, + TState : LoggableState, + TSideEffect : Any, + reified T : MviHost>, +> BehaviorSpecRootScope.GivenViewModel( + noinline viewModel: () -> T, + crossinline test: suspend ViewModelContainerScope.() -> Unit, +): Unit = + addContainer( + TestName("Given: ", "a ${T::class.simpleName}", true), + disabled = false, + null, + ) { + ViewModelContainerScope(this, viewModel()).test() + } diff --git a/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelContainerScope.kt b/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelContainerScope.kt new file mode 100644 index 0000000..1ba4420 --- /dev/null +++ b/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelContainerScope.kt @@ -0,0 +1,59 @@ +package com.adidas.mvi.kotest + +import com.adidas.mvi.Intent +import com.adidas.mvi.LoggableState +import com.adidas.mvi.MviHost +import com.adidas.mvi.State +import io.kotest.core.names.TestName +import io.kotest.core.spec.style.scopes.AbstractContainerScope +import io.kotest.core.test.TestScope + +public class ViewModelContainerScope< + TIntent : Intent, + TState : LoggableState, + TSideEffect : Any, + T : MviHost>, +>( + testScope: TestScope, + private val viewModel: T, +) : AbstractContainerScope(testScope) { + @Suppress("FunctionName") + public suspend fun WhenIntent( + intent: TIntent, + test: suspend ViewModelWhenContainerScope.() -> Unit, + ) { + registerContainer( + name = + TestName( + prefix = "When: ", + name = "${intent::class.simpleName} intent is called", + defaultAffixes = true, + ), + disabled = false, + config = null, + ) { + this@ViewModelContainerScope.viewModel.execute(intent) + ViewModelWhenContainerScope( + testScope = this, + viewModel = this@ViewModelContainerScope.viewModel, + ).test() + } + } + + @Suppress("FunctionName") + public suspend fun When( + name: String, + test: suspend ViewModelWhenContainerScope.() -> Unit, + ) { + registerContainer( + name = TestName(prefix = "When: ", name = name, defaultAffixes = true), + disabled = false, + config = null, + ) { + ViewModelWhenContainerScope( + this, + viewModel = this@ViewModelContainerScope.viewModel, + ).test() + } + } +} diff --git a/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelWhenContainerScope.kt b/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelWhenContainerScope.kt new file mode 100644 index 0000000..7df0054 --- /dev/null +++ b/mvi-kotest/src/main/kotlin/com/adidas/mvi/kotest/ViewModelWhenContainerScope.kt @@ -0,0 +1,65 @@ +package com.adidas.mvi.kotest + +import com.adidas.mvi.Intent +import com.adidas.mvi.LoggableState +import com.adidas.mvi.MviHost +import com.adidas.mvi.State +import io.kotest.core.names.TestName +import io.kotest.core.spec.style.scopes.AbstractContainerScope +import io.kotest.core.test.TestScope +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.flow.StateFlow + +public class ViewModelWhenContainerScope< + TIntent : Intent, + TState : LoggableState, + TSideEffect : Any, + T : MviHost>, +>( + testScope: TestScope, + public val viewModel: T, +) : AbstractContainerScope(testScope) { + @Suppress("FunctionName") + public suspend inline fun ThenState( + vararg sideEffects: TSideEffect, + noinline test: suspend TestScope.(state: State) -> Unit = {}, + ) { + var name = "State should be [${TActualState::class.simpleName}]" + if (sideEffects.isNotEmpty()) { + name += " with sideEffect(s) ${sideEffects.map { it::class.java.simpleName }}" + } + + registerTest( + TestName("Then: ", name, true), + false, + null, + ) { + val state = (this@ViewModelWhenContainerScope.viewModel.state as StateFlow).value + state.view.shouldBeInstanceOf() + sideEffects.forEach { + state.sideEffects.shouldContain(it) + } + + test((this@ViewModelWhenContainerScope.viewModel.state as StateFlow).value as State) + } + } + + @Suppress("FunctionName") + public suspend fun AndIntent( + intent: TIntent, + test: suspend ViewModelWhenContainerScope.() -> Unit, + ) { + registerContainer( + TestName("And: ", "${intent::class.simpleName} intent is executed", true), + false, + null, + ) { + this@ViewModelWhenContainerScope.viewModel.execute(intent) + ViewModelWhenContainerScope( + this, + viewModel = this@ViewModelWhenContainerScope.viewModel, + ).test() + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 47b33ce..417ffd9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,4 @@ dependencyResolutionManagement { rootProject.name = "mvi" include(":mvi") include(":mvi-compose") +include(":mvi-kotest")