Skip to content

Commit

Permalink
Finish up API for Android too
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaudgiuliani committed Sep 13, 2024
1 parent 021bd1c commit 7eb8ed9
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 96 deletions.
53 changes: 4 additions & 49 deletions docs/reference/koin-test/checkmodules.md
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
93 changes: 93 additions & 0 deletions docs/reference/koin-test/verify.md
Original file line number Diff line number Diff line change
@@ -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<Type>(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.ComponentB>(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 ...)
)
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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<KClass<*>> = listOf()) {
org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes)
fun Module.verify(extraTypes: List<KClass<*>> = listOf(), injections: List<ParameterTypeInjection>? = null) {
org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes, injections)
}

/**
Expand All @@ -35,8 +36,8 @@ fun Module.verify(extraTypes: List<KClass<*>> = listOf()) {
* @param extraTypes - allow to declare extra type, to be bound above the existing definitions
* @throws MissingKoinDefinitionException
*/
fun Module.androidVerify(extraTypes: List<KClass<*>> = listOf()) {
org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes)
fun Module.androidVerify(extraTypes: List<KClass<*>> = listOf(), injections: List<ParameterTypeInjection>? = null) {
org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes, injections)
}

object AndroidVerify {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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<MyOtherService>(Id::class)
)
)
}

sampleModule.androidVerify()
@Test
fun `should verify android module`() {
sampleModule.androidVerify(
injections = injectedParameters(
definition<MyOtherService>(Id::class)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ inline fun <reified T> 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 <reified T> definition(injectedParameterTypes : List<KClass<*>>): 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Verification(val module: Module, extraTypes: List<KClass<*>>, injections:
private val allModules: Set<Module> = flatten(module.includedModules.toList()) + module
private val factories: List<InstanceFactory<*>> = allModules.flatMap { it.mappings.values.toList() }
private val extraKeys: List<String> = (extraTypes + Verify.whiteList).map { it.getFullName() }
internal val definitionIndex: List<IndexKey> = allModules.flatMap { it.mappings.keys.toList() } + extraKeys
internal val definitionIndex: List<IndexKey> = allModules.flatMap { it.mappings.keys.toList() }
private val verifiedFactories: HashMap<InstanceFactory<*>, List<KClass<*>>> = hashMapOf()
private val parameterInjectionIndex : Map<String, List<String>> = injections?.associate { inj -> inj.targetType.getFullName() to inj.injectedTypes.map { it.getFullName() }.toList() } ?: emptyMap()

Expand Down Expand Up @@ -50,22 +50,49 @@ class Verification(val module: Module, extraTypes: List<KClass<*>>, 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<String>,
beanDefinition: BeanDefinition<*>,
): List<KClass<*>> {
): List<VerifiedParameter> {
val constructorParameters = constructorFunction.parameters

if (constructorParameters.isEmpty()){
Expand All @@ -75,32 +102,30 @@ class Verification(val module: Module, extraTypes: List<KClass<*>>, 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)
}
}
}
Expand All @@ -116,6 +141,10 @@ class Verification(val module: Module, extraTypes: List<KClass<*>>, 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
}
Loading

0 comments on commit 7eb8ed9

Please sign in to comment.