Skip to content
New issue

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

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

Already on GitHub? # to your account

[Konsist] Integration & Custom Tests #13824

Draft
wants to merge 6 commits into
base: trunk
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .buildkite/commands/run-unit-tests.sh
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ bundle exec fastlane run configure_apply

echo "--- 🧪 Testing"
set +e
./gradlew testJalapenoDebugUnitTest testDebugUnitTest
./gradlew testJalapenoDebugUnitTest testDebugUnitTest -x libs:konsist-test:testDebugUnitTest
TESTS_EXIT_STATUS=$?
set -e

7 changes: 7 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -34,6 +34,13 @@ steps:
agents:
queue: "linter"

- label: "konsist"
command: ./gradlew konsist
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/tests/**/*"
- "**/build/test-results/*/*.xml"

- label: "detekt"
command: ./gradlew detektAll
plugins: [$CI_TOOLKIT]
Original file line number Diff line number Diff line change
@@ -25,9 +25,11 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -60,7 +62,6 @@ import com.woocommerce.android.R
import com.woocommerce.android.extensions.isNotNullOrEmpty
import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview
import com.woocommerce.android.ui.woopos.common.composeui.component.ShadowType
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosCard
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosLazyColumn
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosOutlinedButtonSmall
@@ -73,9 +74,10 @@ import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTyp
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.toAdaptivePadding

@Composable
fun WooPosCartScreen(modifier: Modifier = Modifier) {
val viewModel: WooPosCartViewModel = hiltViewModel()

fun WooPosCartScreen(
modifier: Modifier = Modifier,
viewModel: WooPosCartViewModel = hiltViewModel()
) {
viewModel.state.observeAsState().value?.let {
WooPosCartScreen(modifier, it, viewModel::onUIEvent)
}
@@ -162,10 +164,13 @@ private fun WooPosCartScreen(
end.linkTo(parent.end)
}
) {
WooPosButton(
text = stringResource(R.string.woopos_checkout_button),
Button(
onClick = { onUIEvent(WooPosCartUIEvent.CheckoutClicked) }
)
) {
Text(
text = stringResource(R.string.woopos_checkout_button)
)
}
}
}
}
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -57,6 +57,8 @@ allprojects {
}
}

/* DETEKT */

tasks.register("detektAll", Detekt) {
description = "Custom DETEKT build for all modules"
parallel = true
@@ -80,6 +82,17 @@ dependencies {
detektPlugins project(':libs:detektrules')
}

/* KONSIST */

tasks.register("konsist") {
group = "verification"
description = "Runs Konsist static code analysis."

dependsOn(':libs:konsist-tests:testDebugUnitTest')
}

/* OTHER */

tasks.register('clean', Delete) {
delete rootProject.layout.buildDirectory
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -81,6 +81,7 @@ jna = '5.5.0@aar'
json-path = '2.9.0'
junit = '4.13.2'
lottie = '5.2.0'
konsist = '0.17.3'
kotlin = '2.1.10'
kotlinx-coroutines = '1.8.1'
ksp = '2.1.10-1.0.29'
@@ -242,6 +243,7 @@ jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
json-path = { group = "com.jayway.jsonpath", name = "json-path", version.ref = "json-path" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" }
konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" }
kotlin-test-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
47 changes: 47 additions & 0 deletions libs/konsist-tests/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.dependency.analysis)
}

kotlin {
sourceCompatibility = JvmTarget.fromTarget(libs.versions.java.get()).target
targetCompatibility = JvmTarget.fromTarget(libs.versions.java.get()).target
}

android {
namespace 'com.woocommerce.android.konsist.tests'

compileOptions {
sourceCompatibility libs.versions.java.get()
targetCompatibility libs.versions.java.get()
}

defaultConfig {
minSdkVersion gradle.ext.minSdkVersion
targetSdkVersion gradle.ext.targetSdkVersion
compileSdk gradle.ext.compileSdkVersion
}
}

dependencies {
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.fragment.ktx)
implementation(libs.google.dagger.compiler)
implementation(libs.google.dagger.hilt.compiler)
implementation(libs.google.dagger.hilt.android.main)
implementation(libs.google.dagger.hilt.android.testing)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui.tooling.main)

testImplementation(libs.junit)
testImplementation(libs.konsist)
}

// See: https://docs.konsist.lemonappdev.com/advanced/isolate-konsist-tests#solution-1-module-is-always-out-of-date
tasks.withType(Test).configureEach {
outputs.upToDateWhen { false }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.woocommerce.android.konsist.test

import android.app.Application
import androidx.fragment.app.Fragment
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.properties
import com.lemonappdev.konsist.api.ext.list.withoutAllParentsNamed
import com.lemonappdev.konsist.api.ext.list.withoutAllParentsOf
import com.lemonappdev.konsist.api.ext.list.withoutAnnotationOf
import com.lemonappdev.konsist.api.ext.list.withoutName
import com.lemonappdev.konsist.api.ext.provider.hasAnnotationOf
import com.lemonappdev.konsist.api.verify.assertFalse
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import javax.inject.Inject

class KonsistAnnotationsTest {
@Test // Or suppress 'AppInitializer' class: @Suppress("konsist.no class should use field injection")
fun `no class should use field injection`() {
Konsist.scopeFromProject()
.classes()
.withoutAnnotationOf(HiltAndroidApp::class, AndroidEntryPoint::class, HiltAndroidTest::class)
.withoutAllParentsOf(Application::class)
.withoutAllParentsOf(Fragment::class)
.withoutAllParentsNamed("BaseFragment")
.withoutName("AppInitializer")
.properties()
.assertFalse { it.hasAnnotationOf<Inject>() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.woocommerce.android.konsist.test

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Test

@Suppress("MaxLineLength")
class KonsistComposeTest {
@Test
fun `all jetpack compose previews contain 'preview' in method name`() {
Konsist.scopeFromProject()
.functions()
.withAnnotationOf(Preview::class)
.assertTrue { it.hasNameContaining("Preview") }
}

/**
* [Composable Functions Best Practices ✅](https://github.com/woocommerce/woocommerce-android/blob/trunk/docs/compose.md#composable-functions-best-practices--)
*
* Don't acquire the viewModel inside a composable function, this will make testing harder. Inject it as a parameter
* and provide a default value to facilitate reusability:
*
* ❌
*
* ```
* @Composable
* fun MyComposable() {
* val viewModel by viewModel<MyViewModel>()
* ...
* }
* ```
*
* ✅
*
* ```
* @Composable
* fun MyComposable(viewModel : MyViewModel = getViewModel()) {
* ...
* }
* ```
*/
@Test
fun `composable functions should not acquire view model directly`() {
Konsist.scopeFromProject()
.functions()
.withAnnotationOf(Composable::class)
.assertTrue { function ->
!function.text.contains("""(?i)val\s+\w*viewModel""".toRegex())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.properties
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistFieldsTest {
@Test
fun `no field should have 'm' prefix`() {
Konsist.scopeFromProject()
.classes()
.properties()
.assertFalse {
val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false
it.name.startsWith('m') && secondCharacterIsUppercase
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistFilesTest {
@Test
fun `no empty files allowed`() {
Konsist.scopeFromProject()
.files
.assertFalse { it.text.isEmpty() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.functions
import com.lemonappdev.konsist.api.ext.list.returnTypes
import com.lemonappdev.konsist.api.ext.list.withoutSourceSet
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistFunctionsTest {
@Test // Or suppress all files: @file:Suppress("konsist.return type of all functions are immutable")
fun `return type of all functions - expect in a test - are immutable`() {
Konsist.scopeFromProject()
.files
.filter { file ->
!file.path.contains("FlowExt.kt") &&
!file.path.contains("LiveDataExt.kt") &&
!file.path.contains("SavedStateFlow.kt")
}
.functions()
.returnTypes
.withoutSourceSet("test")
.assertFalse { it.isMutableType }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.declaration.flatten
import com.lemonappdev.konsist.api.ext.list.types
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistGenericsTest {
@Test
fun `property generic type does not contains star projection`() {
Konsist.scopeFromProduction()
.properties()
.types
.assertFalse { type ->
type.typeArguments
?.flatten()
?.any { it.isStarProjection }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.imports
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

@Suppress("MaxLineLength")
class KonsistImportsTest {
@Test // Detekt: WildcardImport (https://detekt.dev/docs/rules/style/#wildcardimport)
fun `no wildcard imports allowed`() {
Konsist.scopeFromProject()
.imports
.assertFalse { it.isWildcard }
}

@Test // Custom Detekt: WooPosDesignSystemButtonUsageRule (https://github.com/woocommerce/woocommerce-android/blob/trunk/libs/detektrules/src/main/kotlin/com/woocommerce/android/detektrules/woopos/WooPosDesignSystemButtonUsageRule.kt#L13)
fun `woopos package - no standard compose buttons imports allowed - use woopos button instead`() {
Konsist.scopeFromPackage("com.woocommerce.android.ui.woopos..")
.files
.filter { file ->
!file.path.contains("WooPosButtons.kt")
}
.imports
.assertFalse { it.hasNameMatching("""^androidx\.compose\.material3\.Button${'$'}""".toRegex()) }
}

@Test // Custom Detekt: WooPosDesignSystemTextUsageRule (https://github.com/woocommerce/woocommerce-android/blob/trunk/libs/detektrules/src/main/kotlin/com/woocommerce/android/detektrules/woopos/WooPosDesignSystemTextUsageRule.kt#L13)
fun `woopos package - no standard compose text imports allowed - use woopos text instead`() {
Konsist.scopeFromPackage("com.woocommerce.android.ui.woopos..")
.files
.filter { file ->
!file.path.contains("WooPosTexts.kt")
}
.imports
.assertFalse { it.hasNameMatching("""^androidx\.compose\.material3\.Text${'$'}""".toRegex()) }
}
}
Loading