From 974129002920c0a60bf6c2bda33536024ebde853 Mon Sep 17 00:00:00 2001 From: npars Date: Wed, 2 Feb 2022 10:17:47 -0400 Subject: [PATCH] Support mocking non-accessible parent methods - Add support for mocking non-accessible parent methods using Mockk - The class being mocked is always searched first for the specified method, then all superclasses are checked in no specific order. If multiple superclasses have matching signatures for the method then there is no guarantee which will be mocked first. - Fixes #425 --- .../kotlin/io/mockk/InternalPlatformDsl.kt | 9 ++- .../io/mockk/it/PrivateParentMethodTest.kt | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 mockk/jvm/src/test/kotlin/io/mockk/it/PrivateParentMethodTest.kt diff --git a/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt b/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt index b4d184ff9..6854e59d9 100644 --- a/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt +++ b/dsl/jvm/src/main/kotlin/io/mockk/InternalPlatformDsl.kt @@ -7,10 +7,12 @@ import java.lang.reflect.Method import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.Continuation import kotlin.reflect.KClass +import kotlin.reflect.KFunction import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty1 import kotlin.reflect.KType import kotlin.reflect.KTypeParameter +import kotlin.reflect.full.allSuperclasses import kotlin.reflect.full.functions import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible @@ -100,7 +102,7 @@ actual object InternalPlatformDsl { anyContinuationGen: () -> Continuation<*> ): Any? { val params = arrayOf(self, *args) - val func = self::class.functions.firstOrNull { + val func = self::class.allAncestorFunctions().firstOrNull { if (it.name != methodName) { return@firstOrNull false } @@ -134,6 +136,11 @@ actual object InternalPlatformDsl { } } + private fun KClass<*>.allAncestorFunctions(): Sequence> { + return (sequenceOf(this) + this.allSuperclasses.asSequence()) + .flatMap { it.functions } + } + private fun List.anyIsInstance(value: Any?): Boolean { return any { bound -> val classifier = bound.classifier diff --git a/mockk/jvm/src/test/kotlin/io/mockk/it/PrivateParentMethodTest.kt b/mockk/jvm/src/test/kotlin/io/mockk/it/PrivateParentMethodTest.kt new file mode 100644 index 000000000..5798337ad --- /dev/null +++ b/mockk/jvm/src/test/kotlin/io/mockk/it/PrivateParentMethodTest.kt @@ -0,0 +1,66 @@ +package io.mockk.it + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Test mocking non-visible parent methods through dynamic calls. Issue #425 + */ +class PrivateParentMethodTest { + open class Parent { + open fun call(): String = callPrivate() + private fun callPrivate() = "Real" + } + + open class Child: Parent() + + class ChildWithShadowedMethod: Parent() { + override fun call(): String = callPrivate() + fun callPrivate() = "Shadowed" + } + + class GrandChild: Child() + + @Test + fun testChildAlwaysMockedFirst() { + val mock = mockk { + every { call() } answers { callOriginal() } + every { this@mockk["callPrivate"]() } returns "Mock" + } + + assertEquals(mock.call(), "Mock") + } + + @Test + fun testPrivateCallMock() { + val mock = mockk { + every { call() } answers { callOriginal() } + every { this@mockk["callPrivate"]() } returns "Mock" + } + + assertEquals(mock.call(), "Mock") + } + + @Test + fun testPrivateCallMockForGrandChild() { + val mock = mockk { + every { call() } answers { callOriginal() } + every { this@mockk["callPrivate"]() } returns "Mock" + } + + assertEquals(mock.call(), "Mock") + } + + @Test + fun testPrivateCallVerify() { + val mock = spyk(Child(), recordPrivateCalls = true) + + mock.call() + + verify { mock["callPrivate"]() } + } +}