Skip to content

Commit

Permalink
Support Mock generation for generic interfaces (#124)
Browse files Browse the repository at this point in the history
This patch allow you to mock classes like `MyInterface<MyType>` the generated mock will have the following naming `MyInterface_MyTypeMock`
  • Loading branch information
MarcoSignoretto authored Sep 27, 2022
1 parent 69dd134 commit c985f01
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
---

## master
* Support mock generation for interfaces with generics

## 2.7.0
* MockingBird plugin support ksp codegen out of the box of google ksp plugin is applied
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.Modifier
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.buildCodeBlock
import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
Expand All @@ -46,11 +48,17 @@ class MockGenerator constructor(
fun createClass(ksTypeRef: KSTypeReference): FileSpec {

val classToMock = ksTypeRef.resolve()

val classGenerics = classToMock.arguments.map { "_${it.type.toString()}" }.joinToString("")

val className = classToMock.toClassName()
val simpleName = className.simpleName

val ksClassDeclaration = classToMock.declaration as KSClassDeclaration

val typeResolver: Map<KSTypeParameter, KSTypeArgument> =
ksClassDeclaration.typeParameters.zip(classToMock.arguments).toMap()

val packageName = className.packageName
val externalClass =
resolver.getClassDeclarationByName(resolver.getKSNameFromString("com.careem.mockingbird.test.Mock"))
Expand All @@ -59,7 +67,7 @@ class MockGenerator constructor(

val (functionsToMock, propertiesToMock) = functionsMiner.extractFunctionsAndProperties(ksClassDeclaration)

val mockClassBuilder = TypeSpec.classBuilder("${simpleName}Mock")
val mockClassBuilder = TypeSpec.classBuilder("${simpleName}${classGenerics}Mock")
.addType(functionsToMock.buildMethodObject())
.addType(functionsToMock.buildArgObject())
.addType(propertiesToMock.buildPropertyObject())
Expand All @@ -73,7 +81,7 @@ class MockGenerator constructor(
}

functionsToMock.forEach { function ->
mockFunction(mockClassBuilder, function, isUnitFunction(function))
mockFunction(typeResolver, mockClassBuilder, function, isUnitFunction(function))
}

val uuid = PropertySpec.builder("uuid", String::class, KModifier.OVERRIDE)
Expand All @@ -88,11 +96,13 @@ class MockGenerator constructor(
mockClassBuilder.addProperty(uuid)

propertiesToMock.forEach { property ->
mockProperty(mockClassBuilder, property)
mockProperty(typeResolver, mockClassBuilder, property)
}

val mockedClass = mockClassBuilder.build()

return FileSpec.builder(packageName, "${simpleName}Mock")
.addType(mockClassBuilder.build())
.addType(mockedClass)
.build()
}

Expand Down Expand Up @@ -200,14 +210,15 @@ class MockGenerator constructor(

@OptIn(KotlinPoetKspPreview::class)
private fun mockProperty(
typeResolver: Map<KSTypeParameter, KSTypeArgument>,
mockClassBuilder: TypeSpec.Builder,
property: KSPropertyDeclaration
) {
logger.info("===> Mocking Property ${property.getter} and ${property.setter}")
val propertyBuilder = PropertySpec
.builder(
property.simpleName.getShortName(),
property.type.resolve().toTypeName(),
property.type.toTypeNameResolved(typeResolver),
KModifier.OVERRIDE
)

Expand Down Expand Up @@ -256,7 +267,7 @@ class MockGenerator constructor(
)
""".trimIndent()
setterBuilder
.addParameter("value", property.type.resolve().toTypeName())
.addParameter("value", property.type.toTypeNameResolved(typeResolver))
.addStatement(setterStatementString, *(setterArgsValue.toTypedArray()))
propertyBuilder
.mutable()
Expand Down Expand Up @@ -291,6 +302,7 @@ class MockGenerator constructor(

@OptIn(KotlinPoetKspPreview::class)
private fun mockFunction(
typeResolver: Map<KSTypeParameter, KSTypeArgument>,
mockClassBuilder: TypeSpec.Builder,
function: KSFunctionDeclaration,
isUnit: Boolean
Expand All @@ -300,17 +312,29 @@ class MockGenerator constructor(
.addModifiers(buildFunctionModifiers(function))
for (valueParam in function.parameters) {
logger.info(valueParam.type.toString())
funBuilder.addParameter(valueParam.name!!.getShortName(), valueParam.type.toTypeName())
funBuilder.addParameter(
valueParam.name!!.getShortName(),
valueParam.type.toTypeNameResolved(typeResolver)
)
}
if (!isUnit) {
funBuilder.returns(function.returnType!!.toTypeName())
funBuilder.returns(function.returnType!!.toTypeNameResolved(typeResolver))
}
funBuilder.addMockStatement(function, isUnit)
mockClassBuilder.addFunction(
funBuilder.build()
)
}

@OptIn(KotlinPoetKspPreview::class)
fun KSTypeReference.toTypeNameResolved(typeResolver: Map<KSTypeParameter, KSTypeArgument>): TypeName {
return if (typeResolver.containsKey(this.resolve().declaration)) {
typeResolver.get(this.resolve().declaration)!!.toTypeName()
} else {
this.toTypeName()
}
}

private fun FunSpec.Builder.addMockStatement(function: KSFunctionDeclaration, isUnit: Boolean) {
val mockFunction = if (isUnit) {
MOCK_UNIT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.careem.mockingbird.kspsample

import kotlin.reflect.KClass

public interface Value

public interface UiDelegate2Args<S : UiState, T: Value> {
var s: S
val t: T

public fun present(uiState: S)
public fun present(uiState: T)
public fun remove(uiStateType: KClass<out UiState>)
public fun ret(): S
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.careem.mockingbird.kspsample

import kotlin.reflect.KClass

public interface UiState

public interface UiDelegate<S : UiState> {
var s: S
val t: S

public fun present(uiState: S)
public fun remove(uiStateType: KClass<out UiState>)
public fun ret(): S
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class KspSampleTest {
@Mock
val innerInnerInterface: InnerInnerInterface = InnerInnerInterfaceMock()

@Mock
val uiDelegate: UiDelegate<UiState> = UiDelegate_UiStateMock()

@Mock
val uiDelegate2Args: UiDelegate2Args<UiState, Value> = UiDelegate2Args_UiState_ValueMock()

@Test
fun testGeneratedTargetProjectMock() {
assertNotNull(pippoMock)
Expand Down

0 comments on commit c985f01

Please sign in to comment.