From 7eb8ed9290db4c160981932bad0fcf8c96d0432f Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Fri, 13 Sep 2024 17:27:00 +0200 Subject: [PATCH] Finish up API for Android too --- docs/reference/koin-test/checkmodules.md | 53 +---------- docs/reference/koin-test/verify.md | 93 +++++++++++++++++++ .../koin/android/test/verify/AndroidVerify.kt | 9 +- .../koin/test/android/AndroidModuleTest.kt | 22 ++++- .../test/verify/ParameterTypeInjection.kt | 10 ++ .../org/koin/test/verify/Verification.kt | 81 ++++++++++------ .../org/koin/test/verify/VerifyModule.kt | 7 +- .../src/jvmTest/kotlin/VerifyModulesTest.kt | 15 ++- 8 files changed, 194 insertions(+), 96 deletions(-) create mode 100644 docs/reference/koin-test/verify.md diff --git a/docs/reference/koin-test/checkmodules.md b/docs/reference/koin-test/checkmodules.md index ad3ea4023..2798daef8 100644 --- a/docs/reference/koin-test/checkmodules.md +++ b/docs/reference/koin-test/checkmodules.md @@ -1,57 +1,12 @@ --- -title: Verifying your Koin configuration +title: CheckModules - Check Koin configuration (Deprecated) --- -:::note -Koin allows you to verify your configuration modules, avoiding discovering dependency injection issues at runtime. +:::warning +This API is now deprecated - since Koin 4.0 ::: - -### Koin Configuration check with Verify() - JVM Only [3.3] - -Use the verify() extension function on a Koin Module. That's it! Under the hood, This will verify all constructor classes and crosscheck with the Koin configuration to know if there is a component declared for this dependency. In case of failure, the function will throw a MissingKoinDefinitionException. - -```kotlin -val niaAppModule = module { - includes( - jankStatsKoinModule, - dataKoinModule, - syncWorkerKoinModule, - topicKoinModule, - authorKoinModule, - interestsKoinModule, - settingsKoinModule, - bookMarksKoinModule, - forYouKoinModule - ) - viewModelOf(::MainActivityViewModel) -} -``` - - -```kotlin -class NiaAppModuleCheck { - - @Test - fun checkKoinModule() { - - // Verify Koin configuration - niaAppModule.verify( - // List types used in definitions but not declared directly (like parameters injection) - extraTypes = listOf(...) - ) - } -} -``` - - -Launch the JUnit test and you're done! ✅ - - -As you may see, we use the extra Types parameter to list types used in the Koin configuration but not declared directly. This is the case for SavedStateHandle and WorkerParameters types, that are used as injected parameters. The Context is declared by androidContext() function at start. - - -The verify() API is ultra light to run and doesn't require any kind of mock/stub to run on your configuration. +Koin allows you to verify your configuration modules, avoiding discovering dependency injection issues at runtime. ### Koin Dynamic Check - CheckModules() diff --git a/docs/reference/koin-test/verify.md b/docs/reference/koin-test/verify.md new file mode 100644 index 000000000..f1c505965 --- /dev/null +++ b/docs/reference/koin-test/verify.md @@ -0,0 +1,93 @@ +--- +title: Verifying your Koin configuration +--- + +Koin allows you to verify your configuration modules, avoiding discovering dependency injection issues at runtime. + +## Koin Configuration check with Verify() - JVM Only [3.3] + +Use the verify() extension function on a Koin Module. That's it! Under the hood, This will verify all constructor classes and crosscheck with the Koin configuration to know if there is a component declared for this dependency. In case of failure, the function will throw a MissingKoinDefinitionException. + +```kotlin +val niaAppModule = module { + includes( + jankStatsKoinModule, + dataKoinModule, + syncWorkerKoinModule, + topicKoinModule, + authorKoinModule, + interestsKoinModule, + settingsKoinModule, + bookMarksKoinModule, + forYouKoinModule + ) + viewModelOf(::MainActivityViewModel) +} +``` + +```kotlin +class NiaAppModuleCheck { + + @Test + fun checkKoinModule() { + + // Verify Koin configuration + niaAppModule.verify() + } +} +``` + + +Launch the JUnit test and you're done! ✅ + + +As you may see, we use the extra Types parameter to list types used in the Koin configuration but not declared directly. This is the case for SavedStateHandle and WorkerParameters types, that are used as injected parameters. The Context is declared by androidContext() function at start. + +The verify() API is ultra light to run and doesn't require any kind of mock/stub to run on your configuration. + +## Verifying with Injected Parameters - JVM Only [4.0] + +When you have a configuration that implies injected obects with `parametersOf`, the verification will fail because there is no definition of the parameter's type in your configuration. +However you can define a parameter type, to be injected with given definition `definition(Class1::class, Class2::class ...)`. + +Here is how it goes: + +```kotlin +class ModuleCheck { + + // given a definition with an injected definition + val module = module { + single { (a: Simple.ComponentA) -> Simple.ComponentB(a) } + } + + @Test + fun checkKoinModule() { + + // Verify and declare Injected Parameters + module.verify( + injections = injectedParameters( + definition(Simple.ComponentA::class) + ) + ) + } +} +``` + +## Type White-Listing + +We can add types as "white-listed". This means that this type is considered as present in the system for any definition. Here is how it goes: + +```kotlin +class NiaAppModuleCheck { + + @Test + fun checkKoinModule() { + + // Verify Koin configuration + niaAppModule.verify( + // List types used in definitions but not declared directly (like parameters injection) + extraTypes = listOf(MyType::class ...) + ) + } +} +``` \ No newline at end of file diff --git a/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt b/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt index a89ad8132..244efb475 100644 --- a/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt +++ b/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt @@ -9,6 +9,7 @@ import androidx.work.WorkerParameters import org.koin.android.test.verify.AndroidVerify.androidTypes import org.koin.core.module.Module import org.koin.test.verify.MissingKoinDefinitionException +import org.koin.test.verify.ParameterTypeInjection import kotlin.reflect.KClass /** @@ -20,8 +21,8 @@ import kotlin.reflect.KClass * @param extraTypes - allow to declare extra type, to be bound above the existing definitions * @throws MissingKoinDefinitionException */ -fun Module.verify(extraTypes: List> = listOf()) { - org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes) +fun Module.verify(extraTypes: List> = listOf(), injections: List? = null) { + org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes, injections) } /** @@ -35,8 +36,8 @@ fun Module.verify(extraTypes: List> = listOf()) { * @param extraTypes - allow to declare extra type, to be bound above the existing definitions * @throws MissingKoinDefinitionException */ -fun Module.androidVerify(extraTypes: List> = listOf()) { - org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes) +fun Module.androidVerify(extraTypes: List> = listOf(), injections: List? = null) { + org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes, injections) } object AndroidVerify { diff --git a/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt b/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt index 5742d223e..b4cc6c7d0 100644 --- a/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt +++ b/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt @@ -13,6 +13,8 @@ import org.koin.core.logger.EmptyLogger import org.koin.dsl.koinApplication import org.koin.dsl.module import org.koin.test.KoinTest +import org.koin.test.verify.definition +import org.koin.test.verify.injectedParameters import org.mockito.Mockito.mock /** @@ -29,17 +31,31 @@ class AndroidModuleTest : KoinTest { single { AndroidComponentB(get()) } single { AndroidComponentC(androidApplication()) } single { OtherService(getProperty(URL)) } + single { p -> MyOtherService(p.get(),get()) } } class AndroidComponentA(val androidContext: Context) class AndroidComponentB(val androidComponent: AndroidComponentA) class AndroidComponentC(val application: Application) class OtherService(val url: String) + class Id + class MyOtherService(val param : Id, val o: OtherService) @Test - fun `should verify android module`() { - sampleModule.verify() + fun `should verify module`() { + sampleModule.verify( + injections = injectedParameters( + definition(Id::class) + ) + ) + } - sampleModule.androidVerify() + @Test + fun `should verify android module`() { + sampleModule.androidVerify( + injections = injectedParameters( + definition(Id::class) + ) + ) } } \ No newline at end of file diff --git a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt index 2f1422571..762d25980 100644 --- a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt +++ b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt @@ -25,6 +25,16 @@ inline fun definition(vararg injectedParameterTypes : KClass<*>): Pa return ParameterTypeInjection(T::class, injectedParameterTypes.toList()) } +/** + * Define injection for a definition Type + * @param T - definition type + * @param injectedParameterTypes - Types that need to be injected later with parametersOf + */ +@KoinExperimentalAPI +inline fun definition(injectedParameterTypes : List>): ParameterTypeInjection{ + return ParameterTypeInjection(T::class, injectedParameterTypes) +} + /** * Declare list of ParameterTypeInjection - in order to help define parmater injection types to allow in verify * @param injectionType - list of ParameterTypeInjection diff --git a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt index 84220ff13..53af6e6a3 100644 --- a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt +++ b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt @@ -21,7 +21,7 @@ class Verification(val module: Module, extraTypes: List>, injections: private val allModules: Set = flatten(module.includedModules.toList()) + module private val factories: List> = allModules.flatMap { it.mappings.values.toList() } private val extraKeys: List = (extraTypes + Verify.whiteList).map { it.getFullName() } - internal val definitionIndex: List = allModules.flatMap { it.mappings.keys.toList() } + extraKeys + internal val definitionIndex: List = allModules.flatMap { it.mappings.keys.toList() } private val verifiedFactories: HashMap, List>> = hashMapOf() private val parameterInjectionIndex : Map> = injections?.associate { inj -> inj.targetType.getFullName() to inj.injectedTypes.map { it.getFullName() }.toList() } ?: emptyMap() @@ -50,22 +50,49 @@ class Verification(val module: Module, extraTypes: List>, injections: val functionType = beanDefinition.primaryType val constructors = functionType.constructors.filter { it.visibility == KVisibility.PUBLIC } - return constructors.flatMap { constructor -> - verifyConstructor( - constructor, - functionType, - index, - beanDefinition + val verifications = constructors + .flatMap { constructor -> + verifyConstructor( + constructor, + functionType, + index ) } + val verificationByStatus = verifications.groupBy { it.status } + verificationByStatus[VerificationStatus.MISSING]?.let { list -> + val first = list.first() + val errorMessage = "Missing definition for '$first' in definition '$beanDefinition'." + val generateParameterInjection = "Fix your Koin configuration or define it as injection for '$beanDefinition':\n${generateInjectionCode(beanDefinition,first)}" + System.err.println("* ----- > $errorMessage\n$generateParameterInjection") + throw MissingKoinDefinitionException(errorMessage) + } + verificationByStatus[VerificationStatus.CIRCULAR]?.let { list -> + val errorMessage = "Circular injection between ${list.first()} and '${functionType.qualifiedName}'.\nFix your Koin configuration!" + System.err.println("* ----- > $errorMessage") + throw CircularInjectionException(errorMessage) + } + + return verificationByStatus[VerificationStatus.OK]?.map { + println("|- dependency '${it.name}' - ${it.type.qualifiedName} found!") + it.type + } ?: emptyList() + } + + private fun generateInjectionCode(beanDefinition: BeanDefinition<*>, p: VerifiedParameter): String { + return """ + module.verify( + injections = injectedParameters( + definition<${beanDefinition.primaryType.qualifiedName}>(${p.type.qualifiedName}::class) + ) + ) + """.trimIndent() } private fun verifyConstructor( constructorFunction: KFunction<*>, functionType: KClass<*>, index: List, - beanDefinition: BeanDefinition<*>, - ): List> { + ): List { val constructorParameters = constructorFunction.parameters if (constructorParameters.isEmpty()){ @@ -75,32 +102,30 @@ class Verification(val module: Module, extraTypes: List>, injections: } return constructorParameters.map { constructorParameter -> + val ctorParamLabel = constructorParameter.name ?: "" val ctorParamClass = (constructorParameter.type.classifier as KClass<*>) val ctorParamFullClassName = ctorParamClass.getFullName() - val isDefinitionDeclared = isClassInDefinitionIndex(index, ctorParamFullClassName) || isClassInInjectionIndex(functionType, ctorParamFullClassName) + val hasDefinition = isClassInDefinitionIndex(index, ctorParamFullClassName) + val isParameterInjected = isClassInInjectionIndex(functionType, ctorParamFullClassName) + if (isParameterInjected){ + println("| dependency '$ctorParamLabel' is injected") + } + val isWhiteList = ctorParamFullClassName in extraKeys + if (isWhiteList){ + println("| dependency '$ctorParamLabel' is whitelisted") + } + val isDefinitionDeclared = hasDefinition || isParameterInjected || isWhiteList + val alreadyBoundFactory = verifiedFactories.keys.firstOrNull { ctorParamClass in listOf(it.beanDefinition.primaryType) + it.beanDefinition.secondaryTypes } val factoryDependencies = verifiedFactories[alreadyBoundFactory] val isCircular = factoryDependencies?.let { functionType in factoryDependencies } ?: false //TODO refactor to attach type / case of error when { - !isDefinitionDeclared -> { - val errorMessage = "Missing definition type '${ctorParamClass.qualifiedName}' in definition '$beanDefinition'" - System.err.println("* ----- > $errorMessage\nFix your Koin configuration or add extraTypes parameter to whitelist the type: verify(extraTypes = listOf(${ctorParamClass.qualifiedName}::class))") - throw MissingKoinDefinitionException(errorMessage) - } - - isCircular -> { - val errorMessage = "Circular injection between '${ctorParamClass.qualifiedName}' and '${functionType.qualifiedName}'. Fix your Koin configuration" - System.err.println("* ----- > $errorMessage") - throw CircularInjectionException(errorMessage) - } - - else -> { - println("|- dependency '$ctorParamClass' found!") - ctorParamClass - } + !isDefinitionDeclared -> VerifiedParameter(ctorParamLabel,ctorParamClass,VerificationStatus.MISSING) + isCircular -> VerifiedParameter(ctorParamLabel,ctorParamClass,VerificationStatus.CIRCULAR) + else -> VerifiedParameter(ctorParamLabel,ctorParamClass,VerificationStatus.OK) } } } @@ -116,6 +141,10 @@ class Verification(val module: Module, extraTypes: List>, injections: index.any { k -> k.contains(ctorParamFullClassName) } } +data class VerifiedParameter(val name : String, val type : KClass<*>, val status: VerificationStatus){ + override fun toString(): String = "[field:'$name' - type:'${type.qualifiedName}']" +} + enum class VerificationStatus { OK, MISSING, CIRCULAR } \ No newline at end of file diff --git a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt index fa522e1c3..dc1fe0c60 100644 --- a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt +++ b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt @@ -1,8 +1,5 @@ -@file:OptIn(KoinExperimentalAPI::class) - package org.koin.test.verify -import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.module.Module import kotlin.reflect.KClass import kotlin.time.measureTime @@ -12,7 +9,7 @@ import kotlin.time.measureTime * Throws MissingDefinitionException if any definition is missing * * @param extraTypes - allow to declare extra type, to be bound above the existing definitions - * @param injections - defines parameters injection types to allow (normally used in parametersOf) + * @param injections - Experimental - defines parameters injection types to allow (normally used in parametersOf) * @throws MissingKoinDefinitionException */ fun Module.verify(extraTypes: List> = listOf(), injections: List? = emptyList()) = Verify.verify(this, extraTypes,injections) @@ -60,7 +57,7 @@ object Verify { * @param extraTypes allow to declare extra type, to be bound above the existing definitions * @throws MissingKoinDefinitionException */ - fun verify(module: Module, extraTypes: List> = listOf(), injections: List?) { + fun verify(module: Module, extraTypes: List> = listOf(), injections: List? = null) { val verification = Verification(module, extraTypes, injections) println("Verifying module '$module' ...") diff --git a/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt b/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt index 25d36c106..c755211ba 100644 --- a/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt +++ b/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt @@ -1,3 +1,4 @@ +import org.koin.core.annotation.KoinExperimentalAPI import kotlin.test.Test import kotlin.test.fail import org.koin.core.module.dsl.factoryOf @@ -145,22 +146,18 @@ class VerifyModulesTest { } } + @OptIn(KoinExperimentalAPI::class) @Test fun verify_one_simple_module_w_inject_param() { val module = module { single { (a: Simple.ComponentA) -> Simple.ComponentB(a) } } - try { - module.verify( - injections = injectedParameters( - definition(Simple.ComponentA::class) - ) + module.verify( + injections = injectedParameters( + definition(Simple.ComponentA::class) ) - } catch (e: Exception) { - System.err.println("$e") - fail("Should not fail to verify module") - } + ) } @Test