Skip to content

Commit

Permalink
Merge pull request #1106 from kkurczewski/inject-mocks-via-ctor
Browse files Browse the repository at this point in the history
Inject mocks via constructor to avoid lateinit var
  • Loading branch information
Raibaz committed Jun 28, 2023
2 parents 4a368dc + ac7cbff commit 3d66a78
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 17 deletions.
61 changes: 44 additions & 17 deletions modules/mockk/src/jvmMain/kotlin/io/mockk/junit5/MockKExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package io.mockk.junit5

import io.mockk.*
import io.mockk.impl.annotations.AdditionalInterface
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import org.junit.jupiter.api.extension.*
import java.lang.annotation.Inherited
import java.lang.reflect.AnnotatedElement
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Parameter
import java.util.Optional
import kotlin.reflect.KClass
import kotlin.reflect.jvm.javaConstructor

/**
* JUnit5 extension.
Expand All @@ -26,6 +30,8 @@ import java.util.Optional
* `–Dmockk.junit.extension.keepmocks=true` on a command line
*/
class MockKExtension : TestInstancePostProcessor, ParameterResolver, AfterAllCallback {
private val cache = mutableMapOf<KClass<out Any>, Any>()

override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
return getMockKAnnotation(parameterContext) != null
}
Expand All @@ -36,28 +42,48 @@ class MockKExtension : TestInstancePostProcessor, ParameterResolver, AfterAllCal
val annotation = getMockKAnnotation(parameterContext) ?: return null
val name = getMockName(parameterContext.parameter, annotation)

val isRelaxed = when (annotation) {
is RelaxedMockK -> true
is MockK -> annotation.relaxed
else -> false
}

val isRelaxedUnitFun = when (annotation) {
is MockK -> annotation.relaxUnitFun
else -> false
}
return when (annotation) {
is InjectMockKs -> tryConstructClass(type)
is SpyK -> spyk(
tryConstructClass(type),
name,
*moreInterfaces(parameterContext),
recordPrivateCalls = annotation.recordPrivateCalls
)
is MockK, is RelaxedMockK -> {
mockkClass(
type,
name,
(annotation as? MockK)?.relaxed ?: true,
*moreInterfaces(parameterContext),
relaxUnitFun = (annotation as? MockK)?.relaxUnitFun ?: false,
)
}
else -> null
}?.also { cache[type] = it }
}

return mockkClass(
type,
name,
isRelaxed,
*moreInterfaces(parameterContext),
relaxUnitFun = isRelaxedUnitFun
private fun tryConstructClass(type: KClass<out Any>): Any = try {
val ctor = type.constructors.first()
val args = ctor.javaConstructor?.parameters
?.map { cache[it.type.kotlin] }
?.toTypedArray()
?: emptyArray()
ctor.call(*args)
} catch (ex: InvocationTargetException) {
// Current JUnit5 implementation resolves test constructor parameters one-by-one in order of declaration.
// This means that any parameter can only access preceding arguments and only those can be used to
// construct non-mock class instance. Breaking this order will cause NPE at test class initialization.
// Same limitation also applies to spies as their use original class implementation under hood.
throw MockKException(
"Unable to instantiate class ${type.simpleName}. " +
"Please ensure that all dependencies needed by class are defined before it in test class constructor. " +
"Already registered mocks: ${cache.values}", ex
)
}

private fun getMockKAnnotation(parameter: ParameterContext): Any? {
return sequenceOf(MockK::class, RelaxedMockK::class)
return sequenceOf(MockK::class, RelaxedMockK::class, SpyK::class, InjectMockKs::class)
.map { parameter.findAnnotation(it.java) }
.firstOrNull { it.isPresent }
?.get()
Expand All @@ -67,6 +93,7 @@ class MockKExtension : TestInstancePostProcessor, ParameterResolver, AfterAllCal
return when {
annotation is MockK -> annotation.name
annotation is RelaxedMockK -> annotation.name
annotation is SpyK -> annotation.name
parameter.isNamePresent -> parameter.name
else -> null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.mockk.junit5

import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import io.mockk.junit5.MockKExtensionConstructorBindingTest.Status.BAD
import io.mockk.junit5.MockKExtensionConstructorBindingTest.Status.CRITICAL
import io.mockk.junit5.MockKExtensionConstructorBindingTest.Status.GOOD
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertEquals

@ExtendWith(MockKExtension::class)
class MockKExtensionConstructorBindingTest(
@MockK private val leftWing: LeftWing,
@SpyK private val rightWing: RightWing,
@RelaxedMockK private val enginePart: EnginePart,
@SpyK private val engine: Engine,
@InjectMockKs private val plane: Plane,
) {

enum class Status {
GOOD, BAD, CRITICAL
}

class LeftWing {
fun status() = GOOD
fun describe() = "Left wing"
}

class RightWing {
fun status() = GOOD
fun describe() = "Right wing"
}

data class Engine(private val enginePart: EnginePart) {
fun status() = enginePart.status()
fun describe() = "Engine" + (enginePart.model() ?: "")
}

class EnginePart {
fun status() = GOOD
fun model(): String? = null
}

class Plane(
private val leftWing: LeftWing,
private val rightWing: RightWing,
private val engine: Engine,
) {
fun describeComponents() = listOf(leftWing.describe(), rightWing.describe(), engine.describe()).joinToString(" ")
fun state() = listOf(leftWing.status(), rightWing.status(), engine.status()).maxByOrNull { it.ordinal } ?: CRITICAL
}

@Test
fun `injects mocks by constructor`() {
every { leftWing.describe() } returns "LW"
every { leftWing.status() } returns GOOD

every { rightWing.describe() } returns "RW"

every { enginePart.status() } returns BAD

assertEquals(plane.state(), BAD)
assertEquals(plane.describeComponents(), "LW RW Engine")
verify { engine.describe() }
}
}

0 comments on commit 3d66a78

Please sign in to comment.