From 463a6eaaa6250160f561ef824a8e57c16381fde7 Mon Sep 17 00:00:00 2001 From: Kishore Padhi Date: Wed, 15 Jan 2025 14:49:52 +0000 Subject: [PATCH 01/21] Fix(Issue #1333 https://github.com/mockk/mockk/issues/1333): Bug fix for the issue with MockK 1.13.16 wraps Results objects twice --- .../kotlin/io/mockk/core/ValueClassSupport.kt | 9 +------ .../kotlin/io/mockk/it/ValueClassTest.kt | 26 ------------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/modules/mockk-core/src/jvmMain/kotlin/io/mockk/core/ValueClassSupport.kt b/modules/mockk-core/src/jvmMain/kotlin/io/mockk/core/ValueClassSupport.kt index 20d95cfd..906a7342 100644 --- a/modules/mockk-core/src/jvmMain/kotlin/io/mockk/core/ValueClassSupport.kt +++ b/modules/mockk-core/src/jvmMain/kotlin/io/mockk/core/ValueClassSupport.kt @@ -36,18 +36,11 @@ actual object ValueClassSupport { val isReturnNullable = kFunction.returnType.isMarkedNullable val isPrimitive = resultType.innermostBoxedClass.java.isPrimitive return if ( - !kFunction.isSuspend && + !(kFunction.isSuspend && isPrimitive) && resultType == expectedReturnType && !(isReturnNullable && isPrimitive) ) { this.boxedValue - } else if ( - (kFunction.isSuspend - && !(isReturnNullable || isPrimitive)) - && (this.javaClass.kotlin == expectedReturnType - && !unboxValueReturnTypes.contains(expectedReturnType)) - ) { - this.boxedValue } else { this } diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/it/ValueClassTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/it/ValueClassTest.kt index a509edcb..62664380 100644 --- a/modules/mockk/src/commonTest/kotlin/io/mockk/it/ValueClassTest.kt +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/it/ValueClassTest.kt @@ -7,15 +7,12 @@ import io.mockk.slot import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertTimeoutPreemptively import java.time.Duration import java.util.UUID import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue class ValueClassTest { @@ -27,29 +24,6 @@ class ValueClassTest { private val dummyValueClassReturn get() = DummyValue(202) private val dummyComplexValueClassReturn get() = ComplexValue(UUID.fromString("25581db2-4cdb-48cd-a6c9-e087aee31f0b")) - private interface Action { - suspend fun execute(params: Params): ReturnType - } - - private class ResultTest : Action> { - override suspend fun execute(params: Unit): Result { - return Result.success("some result") - } - } - - @Nested - inner class TestWithCoEvery { - private val resultTest = mockk { - coEvery { execute(Unit) } coAnswers { Result.success("abc") } - } - - @Test - fun `given a test when mocking value classes with coEvery Result returns then success`() = runTest { - val result = resultTest.execute(Unit) - assertTrue(result.isSuccess) - } - } - // @Test fun `arg is ValueClass, returns ValueClass`() { From 750456cef32f2cbe1cc5a502fb0d1eab74d9a9ee Mon Sep 17 00:00:00 2001 From: Kishore Padhi Date: Fri, 17 Jan 2025 19:32:48 +0000 Subject: [PATCH 02/21] Fix issue# 1329 parallel testing for unmockkAll --- .../mockk/src/jvmMain/kotlin/io/mockk/junit5/MockKExtension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mockk/src/jvmMain/kotlin/io/mockk/junit5/MockKExtension.kt b/modules/mockk/src/jvmMain/kotlin/io/mockk/junit5/MockKExtension.kt index 95e5ae00..37fd2dfa 100644 --- a/modules/mockk/src/jvmMain/kotlin/io/mockk/junit5/MockKExtension.kt +++ b/modules/mockk/src/jvmMain/kotlin/io/mockk/junit5/MockKExtension.kt @@ -127,7 +127,7 @@ class MockKExtension : TestInstancePostProcessor, ParameterResolver, AfterEachCa } private fun finish(context: ExtensionContext) { - if (!context.keepMocks) { + if (!context.keepMocks && !context.requireParallelTesting) { unmockkAll() } From 6e5d7fe981def127d0bdaa6693696d09a5cf21b9 Mon Sep 17 00:00:00 2001 From: Kishore Padhi Date: Tue, 21 Jan 2025 09:46:39 +0000 Subject: [PATCH 03/21] Fix 1329 Added tests --- ...ockKExtensionRequireParallelTestingTest.kt | 57 +++++++++++++++++++ ...ensionWithoutRequireParallelTestingTest.kt | 52 +++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionRequireParallelTestingTest.kt create mode 100644 modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionWithoutRequireParallelTestingTest.kt diff --git a/modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionRequireParallelTestingTest.kt b/modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionRequireParallelTestingTest.kt new file mode 100644 index 00000000..17f8d4ae --- /dev/null +++ b/modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionRequireParallelTestingTest.kt @@ -0,0 +1,57 @@ +package io.mockk.junit5 + +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +@MockKExtension.RequireParallelTesting +class MockKExtensionRequireParallelTestingTest { + @MockK + private lateinit var car: Car + + @BeforeEach + fun setUp() { + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @AfterEach + fun tearDown() { + Dispatchers.resetMain() + } + + companion object { + @JvmStatic + @AfterAll + fun afterAll() { + Dispatchers.shutdown() + } + } + + @Test + fun `given car when test with require parallel testing execution returns successfully`() = runTest { + // Given + every { car.drive() } returns "driving" + + // When + val result = car.drive() + + // Then + verify { car.drive() } + assert(result == "driving") + } +} + +internal interface Car { + fun drive(): String +} \ No newline at end of file diff --git a/modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionWithoutRequireParallelTestingTest.kt b/modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionWithoutRequireParallelTestingTest.kt new file mode 100644 index 00000000..cb15a5dd --- /dev/null +++ b/modules/mockk/src/jvmTest/kotlin/io/mockk/junit5/MockKExtensionWithoutRequireParallelTestingTest.kt @@ -0,0 +1,52 @@ +package io.mockk.junit5 + +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class MockKExtensionWithoutRequireParallelTestingTest { + @MockK + private lateinit var car: Car + + @BeforeEach + fun setUp() { + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @AfterEach + fun tearDown() { + Dispatchers.resetMain() + } + + companion object { + @JvmStatic + @AfterAll + fun afterAll() { + Dispatchers.shutdown() + } + } + + @Test + fun `given car when test without require parallel testing execution returns successfully`() = runTest { + // Given + every { car.drive() } returns "driving" + + // When + val result = car.drive() + + // Then + verify { car.drive() } + assert(result == "driving") + } +} \ No newline at end of file From 98278e74b90aa1ee37b048fea064578ff0de743f Mon Sep 17 00:00:00 2001 From: devtaebong Date: Fri, 31 Jan 2025 01:53:40 +0900 Subject: [PATCH 04/21] feat: Add restricted class detection for mocking - Implement `RestrictedMockClasses` to maintain a list of restricted classes - Use `isAssignableFrom` to check for restricted class types dynamically - Allow adding and removing user-defined restricted types - Include default restricted types: System, Collections, I/O classes --- .../src/commonMain/kotlin/io/mockk/MockK.kt | 3 + .../impl/restrict/RestrictedMockClasses.kt | 44 ++++++++++ .../restrict/RestrictedMockClassesTest.kt | 82 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt create mode 100644 modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt index 3e5c86c6..9133c77c 100644 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt @@ -2,6 +2,7 @@ package io.mockk +import io.mockk.impl.restrict.RestrictedMockClasses import kotlin.reflect.KClass /** @@ -39,6 +40,8 @@ inline fun mockk( relaxUnitFun: Boolean = false, block: T.() -> Unit = {} ): T = MockK.useImpl { + RestrictedMockClasses.warnIfRestricted(T::class.java) + MockKDsl.internalMockk( name, relaxed, diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt new file mode 100644 index 00000000..f73dad82 --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt @@ -0,0 +1,44 @@ +package io.mockk.impl.restrict + +import java.io.File +import java.nio.file.Path +import java.util.logging.Logger + +object RestrictedMockClasses { + private val logger = Logger.getLogger(RestrictedMockClasses::class.java.name) + + private val defaultRestrictedTypes: Set> = setOf( + System::class.java, + + Collection::class.java, + Map::class.java, + + File::class.java, + Path::class.java + ) + + private val userDefinedRestrictedTypes = mutableSetOf>() + + fun warnIfRestricted(clazz: Class<*>) { + if (isRestricted(clazz)) { + logger.warning("Warning: Attempting to mock a restricted class (${clazz.name}). This is usually a bad practice.") + } + } + + fun isRestricted(clazz: Class<*>): Boolean { + return defaultRestrictedTypes.any { it.isAssignableFrom(clazz) } || + userDefinedRestrictedTypes.any { it.isAssignableFrom(clazz) } + } + + fun addRestrictedType(clazz: Class<*>) { + userDefinedRestrictedTypes.add(clazz) + } + + fun removeRestrictedType(clazz: Class<*>) { + userDefinedRestrictedTypes.remove(clazz) + } + + fun clearUserDefinedRestrictions() { + userDefinedRestrictedTypes.clear() + } +} \ No newline at end of file diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt new file mode 100644 index 00000000..a49c70fe --- /dev/null +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt @@ -0,0 +1,82 @@ +package io.mockk.restrict + +import io.mockk.impl.restrict.RestrictedMockClasses +import io.mockk.mockk +import kotlin.test.Test +import java.io.File +import java.nio.file.Path +import java.util.* +import java.util.logging.Level +import java.util.logging.Logger +import java.util.logging.SimpleFormatter +import java.util.logging.StreamHandler +import kotlin.collections.ArrayList +import kotlin.collections.HashSet +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class RestrictedMockClassesTest { + + private fun captureLogs(action: () -> Unit): String { + val logger = Logger.getLogger(RestrictedMockClasses::class.java.name) + val logCapture = StringBuilder() + val handler = object : StreamHandler() { + override fun publish(record: java.util.logging.LogRecord) { + logCapture.append(SimpleFormatter().format(record)) + } + } + + logger.level = Level.WARNING + logger.addHandler(handler) + + action() + + handler.flush() + logger.removeHandler(handler) + return logCapture.toString() + } + + @Test + fun `should detect default restricted types`() { + assertTrue { RestrictedMockClasses.isRestricted(System::class.java) } + + assertTrue { RestrictedMockClasses.isRestricted(ArrayList::class.java) } + assertTrue { RestrictedMockClasses.isRestricted(HashSet::class.java) } + assertTrue { RestrictedMockClasses.isRestricted(TreeMap::class.java) } + assertTrue { RestrictedMockClasses.isRestricted(LinkedList::class.java) } + + assertTrue { RestrictedMockClasses.isRestricted(File::class.java) } + assertTrue { RestrictedMockClasses.isRestricted(Path::class.java) } + } + + @Test + fun `should allow adding and removing custom restricted types`() { + val customClass = String::class.java + + RestrictedMockClasses.addRestrictedType(customClass) + assertTrue { RestrictedMockClasses.isRestricted(customClass) } + + RestrictedMockClasses.removeRestrictedType(customClass) + assertFalse { RestrictedMockClasses.isRestricted(customClass) } + } + + @Test + fun `should clear user-defined restricted types while keeping defaults`() { + val customClass = UUID::class.java + + RestrictedMockClasses.addRestrictedType(customClass) + RestrictedMockClasses.clearUserDefinedRestrictions() + + assertFalse { RestrictedMockClasses.isRestricted(customClass) } + assertTrue { RestrictedMockClasses.isRestricted(System::class.java) } + } + + @Test + fun `should log warning when attempting to mock restricted class`() { + val logOutput = captureLogs { + RestrictedMockClasses.warnIfRestricted(File::class.java) + } + + assertTrue { "Warning: Attempting to mock a restricted class (java.io.File)" in logOutput } + } +} From 10bd66edbaa65deb8837d8b53b759a0f376f0da3 Mon Sep 17 00:00:00 2001 From: devtaebong Date: Fri, 31 Jan 2025 02:12:05 +0900 Subject: [PATCH 05/21] feat: Add `disallowMockingRestrictedClasses` setting to MockKSettings - Added `disallowMockingRestrictedClasses` property to `MockKSettings` - Loaded setting from `settings.properties` for better configurability - Integrated setting into `RestrictedMockClasses` to prevent restricted class mocking - If enabled, throws `IllegalArgumentException` instead of just logging a warning --- .../kotlin/io/mockk/MockKSettings.kt | 2 ++ .../jvmMain/kotlin/io/mockk/MockKSettings.kt | 6 ++++ .../src/commonMain/kotlin/io/mockk/MockK.kt | 2 +- .../impl/restrict/RestrictedMockClasses.kt | 9 +++-- .../restrict/RestrictedMockClassesTest.kt | 35 +++++++++++++++---- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt index 608ae715..407d14f8 100644 --- a/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt +++ b/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt @@ -10,6 +10,8 @@ expect object MockKSettings { val stackTracesOnVerify: Boolean val stackTracesAlignment: StackTracesAlignment + + val disallowMockingRestrictedClasses: Boolean } enum class StackTracesAlignment { diff --git a/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt index 76e6ff13..e8868f8f 100644 --- a/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt +++ b/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt @@ -33,6 +33,12 @@ actual object MockKSettings { actual val stackTracesAlignment: StackTracesAlignment get() = stackTracesAlignmentValueOf(properties.getProperty("stackTracesAlignment", "center")) + actual val disallowMockingRestrictedClasses: Boolean + get() = booleanProperty("disallowMockingRestrictedClasses", "false") + + fun setDisallowMockingRestrictedClasses(value: Boolean) { + properties.setProperty("disallowMockingRestrictedClasses", value.toString()) + } fun setRelaxed(value: Boolean) { properties.setProperty("relaxed", value.toString()) diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt index 9133c77c..1b13f980 100644 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt @@ -40,7 +40,7 @@ inline fun mockk( relaxUnitFun: Boolean = false, block: T.() -> Unit = {} ): T = MockK.useImpl { - RestrictedMockClasses.warnIfRestricted(T::class.java) + RestrictedMockClasses.handleRestrictedMocking(T::class.java) MockKDsl.internalMockk( name, diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt index f73dad82..1708e132 100644 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt @@ -1,5 +1,6 @@ package io.mockk.impl.restrict +import io.mockk.MockKSettings import java.io.File import java.nio.file.Path import java.util.logging.Logger @@ -19,9 +20,13 @@ object RestrictedMockClasses { private val userDefinedRestrictedTypes = mutableSetOf>() - fun warnIfRestricted(clazz: Class<*>) { + fun handleRestrictedMocking(clazz: Class<*>) { if (isRestricted(clazz)) { - logger.warning("Warning: Attempting to mock a restricted class (${clazz.name}). This is usually a bad practice.") + if (MockKSettings.disallowMockingRestrictedClasses) { + throw IllegalArgumentException("Cannot mock restricted class: ${clazz.name}") + } else { + logger.warning("Warning: Attempting to mock a restricted class (${clazz.name}). This is usually a bad practice.") + } } } diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt index a49c70fe..b603cf12 100644 --- a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt @@ -1,8 +1,8 @@ package io.mockk.restrict +import io.mockk.MockKSettings import io.mockk.impl.restrict.RestrictedMockClasses -import io.mockk.mockk -import kotlin.test.Test +import org.junit.jupiter.api.assertThrows import java.io.File import java.nio.file.Path import java.util.* @@ -10,8 +10,8 @@ import java.util.logging.Level import java.util.logging.Logger import java.util.logging.SimpleFormatter import java.util.logging.StreamHandler -import kotlin.collections.ArrayList -import kotlin.collections.HashSet +import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -72,11 +72,34 @@ class RestrictedMockClassesTest { } @Test - fun `should log warning when attempting to mock restricted class`() { + fun `should log warning when attempting to mock restricted class if setting is default`() { + val logOutput = captureLogs { + RestrictedMockClasses.handleRestrictedMocking(File::class.java) + } + + assertTrue { "Warning: Attempting to mock a restricted class (java.io.File)" in logOutput } + } + + @Test + fun `should log warning when mocking restricted class if setting is disabled`() { val logOutput = captureLogs { - RestrictedMockClasses.warnIfRestricted(File::class.java) + RestrictedMockClasses.handleRestrictedMocking(File::class.java) } + MockKSettings.setDisallowMockingRestrictedClasses(false) + + RestrictedMockClasses.handleRestrictedMocking(File::class.java) assertTrue { "Warning: Attempting to mock a restricted class (java.io.File)" in logOutput } } + + @Test + fun `should throws an exception when attempting to mock restricted class if setting is true`() { + MockKSettings.setDisallowMockingRestrictedClasses(true) + + val ex = assertThrows { + RestrictedMockClasses.handleRestrictedMocking(File::class.java) + } + + assertEquals(ex.message, "Cannot mock restricted class: java.io.File") + } } From ef312b6bd7b788638a3b85c0a9c59e7f1202ed2b Mon Sep 17 00:00:00 2001 From: devtaebong Date: Fri, 31 Jan 2025 02:23:17 +0900 Subject: [PATCH 06/21] test: Add test case for user-defined restricted class exception handling - Added a test to verify that attempting to mock a user-defined restricted class throws an `IllegalArgumentException` when `disallowMockingRestrictedClasses` is enabled. - Ensures that dynamically added restricted classes are properly handled. - Validates that the correct exception message is returned. --- .../io/mockk/restrict/RestrictedMockClassesTest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt index b603cf12..73a1b1a9 100644 --- a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt @@ -102,4 +102,17 @@ class RestrictedMockClassesTest { assertEquals(ex.message, "Cannot mock restricted class: java.io.File") } + + @Test + fun `should throw an exception when mocking a user-defined restricted class if setting is enabled`() { + MockKSettings.setDisallowMockingRestrictedClasses(true) + val customClass = UUID::class.java + RestrictedMockClasses.addRestrictedType(customClass) + + val ex = assertThrows { + RestrictedMockClasses.handleRestrictedMocking(customClass) + } + + assertEquals(ex.message, "Cannot mock restricted class: java.util.UUID") + } } From b68c7a60107f06658e627a800d8bb018ebb699ce Mon Sep 17 00:00:00 2001 From: devtaebong Date: Fri, 31 Jan 2025 03:02:01 +0900 Subject: [PATCH 07/21] "chore: Update API dump for mockk-dsl after restricted mocking changes" --- modules/mockk-dsl/api/mockk-dsl.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/mockk-dsl/api/mockk-dsl.api b/modules/mockk-dsl/api/mockk-dsl.api index 50da55ff..8ee9e3e5 100644 --- a/modules/mockk-dsl/api/mockk-dsl.api +++ b/modules/mockk-dsl/api/mockk-dsl.api @@ -924,11 +924,13 @@ public final class io/mockk/MockKObjectScope : io/mockk/MockKUnmockKScope { public final class io/mockk/MockKSettings { public static final field INSTANCE Lio/mockk/MockKSettings; + public final fun getDisallowMockingRestrictedClasses ()Z public final fun getRecordPrivateCalls ()Z public final fun getRelaxUnitFun ()Z public final fun getRelaxed ()Z public final fun getStackTracesAlignment ()Lio/mockk/StackTracesAlignment; public final fun getStackTracesOnVerify ()Z + public final fun setDisallowMockingRestrictedClasses (Z)V public final fun setRecordPrivateCalls (Z)V public final fun setRelaxUnitFun (Z)V public final fun setRelaxed (Z)V From 2a6f62f395d4e9e69bcf86c72c7637db9e344e89 Mon Sep 17 00:00:00 2001 From: devtaebong Date: Fri, 31 Jan 2025 14:18:07 +0900 Subject: [PATCH 08/21] chore: Update API dump for mockk-dsl after restricted mocking changes --- modules/mockk/api/mockk.api | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/mockk/api/mockk.api b/modules/mockk/api/mockk.api index 2faf80ff..50939e08 100644 --- a/modules/mockk/api/mockk.api +++ b/modules/mockk/api/mockk.api @@ -912,6 +912,15 @@ public final class io/mockk/impl/recording/states/VerifyingState$Companion { public final fun getLog ()Lio/mockk/impl/log/Logger; } +public final class io/mockk/impl/restrict/RestrictedMockClasses { + public static final field INSTANCE Lio/mockk/impl/restrict/RestrictedMockClasses; + public final fun addRestrictedType (Ljava/lang/Class;)V + public final fun clearUserDefinedRestrictions ()V + public final fun handleRestrictedMocking (Ljava/lang/Class;)V + public final fun isRestricted (Ljava/lang/Class;)Z + public final fun removeRestrictedType (Ljava/lang/Class;)V +} + public final class io/mockk/impl/stub/AnswerAnsweringOpportunity : io/mockk/Answer, io/mockk/MockKGateway$AnswerOpportunity { public fun (Lkotlin/jvm/functions/Function0;)V public fun answer (Lio/mockk/Call;)Ljava/lang/Object; From 9e934db7095539698dcfb1bcba12bcb8182853bb Mon Sep 17 00:00:00 2001 From: devtaebong Date: Tue, 4 Feb 2025 00:55:21 +0900 Subject: [PATCH 09/21] fix: ensure test atomicity by resetting restricted class settings per test --- .../mockk/impl/annotations/MockkRestricted.kt | 15 ++++ .../restrict/MockingRestrictedExtension.kt | 33 +++++++++ .../impl/restrict/RestrictedMockClasses.kt | 17 ++--- .../restrict/RestrictedMockAtomicTest.kt | 70 +++++++++++++++++++ .../restrict/RestrictedMockClassesTest.kt | 69 ++++++++---------- 5 files changed, 153 insertions(+), 51 deletions(-) create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt create mode 100644 modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt new file mode 100644 index 00000000..6f7b96bd --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt @@ -0,0 +1,15 @@ +package io.mockk.impl.annotations + +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +annotation class MockkRestricted( + val mode: MockkRestrictedMode = MockkRestrictedMode.WARN, + val restricted: Array> = [] +) + +enum class MockkRestrictedMode { + WARN, + EXCEPTION, +} diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt new file mode 100644 index 00000000..bd2fd2d9 --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt @@ -0,0 +1,33 @@ +package io.mockk.impl.restrict + +import io.mockk.MockKSettings +import io.mockk.impl.annotations.MockkRestricted +import io.mockk.impl.annotations.MockkRestrictedMode +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import java.lang.reflect.Method + +class MockingRestrictedExtension : BeforeEachCallback { + + override fun beforeEach(context: ExtensionContext) { + val testMethod: Method? = context.testMethod.orElse(null) + val testClass = context.testClass.orElse(null) + + MockKSettings.setDisallowMockingRestrictedClasses(false) + RestrictedMockClasses.resetUserDefinedRestrictedTypes() + + val annotation = testMethod?.getAnnotation(MockkRestricted::class.java) + ?: testClass?.getAnnotation(MockkRestricted::class.java) + + annotation?.let { + when (it.mode) { + MockkRestrictedMode.EXCEPTION -> MockKSettings.setDisallowMockingRestrictedClasses(true) + MockkRestrictedMode.WARN -> MockKSettings.setDisallowMockingRestrictedClasses(false) + } + + val restrictedClasses = it.restricted.map { clazz -> clazz.java } + RestrictedMockClasses.setUserDefinedRestrictedTypes(restrictedClasses) + } + } +} + diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt index 1708e132..8b9314f9 100644 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt @@ -8,12 +8,10 @@ import java.util.logging.Logger object RestrictedMockClasses { private val logger = Logger.getLogger(RestrictedMockClasses::class.java.name) - private val defaultRestrictedTypes: Set> = setOf( + private val defaultRestrictedTypes: MutableSet> = mutableSetOf( System::class.java, - Collection::class.java, Map::class.java, - File::class.java, Path::class.java ) @@ -35,15 +33,12 @@ object RestrictedMockClasses { userDefinedRestrictedTypes.any { it.isAssignableFrom(clazz) } } - fun addRestrictedType(clazz: Class<*>) { - userDefinedRestrictedTypes.add(clazz) - } - - fun removeRestrictedType(clazz: Class<*>) { - userDefinedRestrictedTypes.remove(clazz) + fun setUserDefinedRestrictedTypes(classes: List>) { + userDefinedRestrictedTypes.clear() + userDefinedRestrictedTypes.addAll(classes) } - fun clearUserDefinedRestrictions() { + fun resetUserDefinedRestrictedTypes() { userDefinedRestrictedTypes.clear() } -} \ No newline at end of file +} diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt new file mode 100644 index 00000000..057e025c --- /dev/null +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt @@ -0,0 +1,70 @@ +package io.mockk.restrict + +import io.mockk.impl.annotations.MockkRestricted +import io.mockk.impl.annotations.MockkRestrictedMode +import io.mockk.impl.restrict.MockingRestrictedExtension +import io.mockk.mockk +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import java.io.File +import kotlin.test.Test + +@ExtendWith(MockingRestrictedExtension::class) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class RestrictedMockAtomicTest { + @Test + @Order(1) + @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION) + fun test1() { + assertThrows { + mockk() + } + } + + @Test + @Order(2) + @MockkRestricted + fun test2() { + mockk() + } + + @Test + @Order(3) + @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION, restricted = [Foo::class]) + fun test3() { + assertThrows{ + mockk() + } + } + + @Test + @Order(4) + fun test4() { + assertDoesNotThrow { + mockk() + } + } + + @Test + @Order(5) + @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION) + fun test5() { + assertThrows { + mockk() + } + } + + @Test + @Order(6) + fun test6() { + assertDoesNotThrow { + mockk() + } + } + + class Foo +} diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt index 73a1b1a9..c01d79b9 100644 --- a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt @@ -1,8 +1,14 @@ package io.mockk.restrict import io.mockk.MockKSettings +import io.mockk.impl.annotations.MockkRestricted +import io.mockk.impl.annotations.MockkRestrictedMode +import io.mockk.impl.restrict.MockingRestrictedExtension import io.mockk.impl.restrict.RestrictedMockClasses +import io.mockk.mockk +import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith import java.io.File import java.nio.file.Path import java.util.* @@ -12,9 +18,9 @@ import java.util.logging.SimpleFormatter import java.util.logging.StreamHandler import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue +@ExtendWith(MockingRestrictedExtension::class) class RestrictedMockClassesTest { private fun captureLogs(action: () -> Unit): String { @@ -50,69 +56,52 @@ class RestrictedMockClassesTest { } @Test - fun `should allow adding and removing custom restricted types`() { - val customClass = String::class.java - - RestrictedMockClasses.addRestrictedType(customClass) - assertTrue { RestrictedMockClasses.isRestricted(customClass) } - - RestrictedMockClasses.removeRestrictedType(customClass) - assertFalse { RestrictedMockClasses.isRestricted(customClass) } + fun `should log warning when attempting to mock restricted class if setting is default`() { + assertDoesNotThrow { mockk() } } @Test - fun `should clear user-defined restricted types while keeping defaults`() { - val customClass = UUID::class.java - - RestrictedMockClasses.addRestrictedType(customClass) - RestrictedMockClasses.clearUserDefinedRestrictions() - - assertFalse { RestrictedMockClasses.isRestricted(customClass) } - assertTrue { RestrictedMockClasses.isRestricted(System::class.java) } + fun `If the annotation is not used, a warning log is recorded`() { + assertDoesNotThrow { mockk() } } @Test - fun `should log warning when attempting to mock restricted class if setting is default`() { - val logOutput = captureLogs { - RestrictedMockClasses.handleRestrictedMocking(File::class.java) + @MockkRestricted(mode = MockkRestrictedMode.WARN) + fun `WARN value of the annotation is a warning log`() { + assertDoesNotThrow { + mockk() } - - assertTrue { "Warning: Attempting to mock a restricted class (java.io.File)" in logOutput } } @Test - fun `should log warning when mocking restricted class if setting is disabled`() { - val logOutput = captureLogs { - RestrictedMockClasses.handleRestrictedMocking(File::class.java) + @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION) + fun `If the annotation's option is set, it should throw an IllegalArgumentException`() { + assertThrows { + mockk() } - - MockKSettings.setDisallowMockingRestrictedClasses(false) - - RestrictedMockClasses.handleRestrictedMocking(File::class.java) - assertTrue { "Warning: Attempting to mock a restricted class (java.io.File)" in logOutput } } @Test - fun `should throws an exception when attempting to mock restricted class if setting is true`() { - MockKSettings.setDisallowMockingRestrictedClasses(true) + @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION, restricted = [UUID::class, RestrictedMockAtomicTest.Foo::class]) + fun `custom classes can be configured`() { - val ex = assertThrows { - RestrictedMockClasses.handleRestrictedMocking(File::class.java) + assertDoesNotThrow { + mockk() } - assertEquals(ex.message, "Cannot mock restricted class: java.io.File") + assertThrows { + mockk() + } } @Test - fun `should throw an exception when mocking a user-defined restricted class if setting is enabled`() { + fun `should throws an exception when attempting to mock restricted class if setting is true`() { MockKSettings.setDisallowMockingRestrictedClasses(true) - val customClass = UUID::class.java - RestrictedMockClasses.addRestrictedType(customClass) val ex = assertThrows { - RestrictedMockClasses.handleRestrictedMocking(customClass) + RestrictedMockClasses.handleRestrictedMocking(File::class.java) } - assertEquals(ex.message, "Cannot mock restricted class: java.util.UUID") + assertEquals(ex.message, "Cannot mock restricted class: java.io.File") } } From 6e256dd94ad79d8ede99a39bf85cdd1a80c10434 Mon Sep 17 00:00:00 2001 From: devtaebong Date: Tue, 4 Feb 2025 01:21:34 +0900 Subject: [PATCH 10/21] Update API dump after RestrictedMockClasses change --- modules/mockk/api/mockk.api | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/modules/mockk/api/mockk.api b/modules/mockk/api/mockk.api index 50939e08..71f42cba 100644 --- a/modules/mockk/api/mockk.api +++ b/modules/mockk/api/mockk.api @@ -226,6 +226,18 @@ public abstract interface annotation class io/mockk/impl/annotations/MockK : jav public abstract fun relaxed ()Z } +public abstract interface annotation class io/mockk/impl/annotations/MockkRestricted : java/lang/annotation/Annotation { + public abstract fun mode ()Lio/mockk/impl/annotations/MockkRestrictedMode; + public abstract fun restricted ()[Ljava/lang/Class; +} + +public final class io/mockk/impl/annotations/MockkRestrictedMode : java/lang/Enum { + public static final field EXCEPTION Lio/mockk/impl/annotations/MockkRestrictedMode; + public static final field WARN Lio/mockk/impl/annotations/MockkRestrictedMode; + public static fun valueOf (Ljava/lang/String;)Lio/mockk/impl/annotations/MockkRestrictedMode; + public static fun values ()[Lio/mockk/impl/annotations/MockkRestrictedMode; +} + public abstract interface annotation class io/mockk/impl/annotations/OverrideMockKs : java/lang/annotation/Annotation { public abstract fun injectImmutable ()Z public abstract fun lookupType ()Lio/mockk/impl/annotations/InjectionLookupType; @@ -912,13 +924,17 @@ public final class io/mockk/impl/recording/states/VerifyingState$Companion { public final fun getLog ()Lio/mockk/impl/log/Logger; } +public final class io/mockk/impl/restrict/MockingRestrictedExtension : org/junit/jupiter/api/extension/BeforeEachCallback { + public fun ()V + public fun beforeEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V +} + public final class io/mockk/impl/restrict/RestrictedMockClasses { public static final field INSTANCE Lio/mockk/impl/restrict/RestrictedMockClasses; - public final fun addRestrictedType (Ljava/lang/Class;)V - public final fun clearUserDefinedRestrictions ()V public final fun handleRestrictedMocking (Ljava/lang/Class;)V public final fun isRestricted (Ljava/lang/Class;)Z - public final fun removeRestrictedType (Ljava/lang/Class;)V + public final fun resetUserDefinedRestrictedTypes ()V + public final fun setUserDefinedRestrictedTypes (Ljava/util/List;)V } public final class io/mockk/impl/stub/AnswerAnsweringOpportunity : io/mockk/Answer, io/mockk/MockKGateway$AnswerOpportunity { From 4e5fab7ca03a71f3fab43af8254da2120dd8bcb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=83=9C=ED=98=84=28TaeHyeon=2C=20Kim=29?= Date: Wed, 5 Feb 2025 23:01:06 +0900 Subject: [PATCH 11/21] feat: Implement global mock restriction handling - Introduced `MockkValidator` to enforce global mock restrictions. - Automatically throws `MockKException` or logs a warning if a restricted class is mocked. - Updated `RestrictMockkConfiguration` to support user-defined restricted classes. - Added support for hierarchical type checking (e.g., restricting `Collection` applies to `ArrayList`). - Implemented `TestPropertiesLoader` for dynamic test configurations. - Refactored tests: separated collection-related tests into `RestrictMockkCollectionTest`. --- .../kotlin/io/mockk/MockKSettings.kt | 2 - .../jvmMain/kotlin/io/mockk/MockKSettings.kt | 8 - .../src/commonMain/kotlin/io/mockk/MockK.kt | 12 +- .../mockk/impl/annotations/MockkRestricted.kt | 15 -- .../restrict/MockingRestrictedExtension.kt | 33 ---- .../io/mockk/impl/restrict/MockkValidator.kt | 35 ++++ .../restrict/RestrictMockkConfiguration.kt | 41 +++++ .../impl/restrict/RestrictedMockClasses.kt | 44 ----- .../DefaultPropertiesLoader.kt | 12 ++ .../propertiesloader/PropertiesLoader.kt | 7 + .../io/mockk/restrict/RestrictMockkTest.kt | 161 ++++++++++++++++++ .../restrict/RestrictedMockAtomicTest.kt | 70 -------- .../restrict/RestrictedMockClassesTest.kt | 107 ------------ .../io/mockk/restrict/TestPropertiesLoader.kt | 14 ++ 14 files changed, 276 insertions(+), 285 deletions(-) delete mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt delete mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockkValidator.kt create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictMockkConfiguration.kt delete mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/DefaultPropertiesLoader.kt create mode 100644 modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/PropertiesLoader.kt create mode 100644 modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictMockkTest.kt delete mode 100644 modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt delete mode 100644 modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt create mode 100644 modules/mockk/src/commonTest/kotlin/io/mockk/restrict/TestPropertiesLoader.kt diff --git a/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt index 407d14f8..608ae715 100644 --- a/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt +++ b/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt @@ -10,8 +10,6 @@ expect object MockKSettings { val stackTracesOnVerify: Boolean val stackTracesAlignment: StackTracesAlignment - - val disallowMockingRestrictedClasses: Boolean } enum class StackTracesAlignment { diff --git a/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt index e8868f8f..979bdd20 100644 --- a/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt +++ b/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt @@ -17,7 +17,6 @@ actual object MockKSettings { defaultValue )!!.toBoolean() - actual val relaxed: Boolean get() = booleanProperty("relaxed", "false") @@ -33,13 +32,6 @@ actual object MockKSettings { actual val stackTracesAlignment: StackTracesAlignment get() = stackTracesAlignmentValueOf(properties.getProperty("stackTracesAlignment", "center")) - actual val disallowMockingRestrictedClasses: Boolean - get() = booleanProperty("disallowMockingRestrictedClasses", "false") - - fun setDisallowMockingRestrictedClasses(value: Boolean) { - properties.setProperty("disallowMockingRestrictedClasses", value.toString()) - } - fun setRelaxed(value: Boolean) { properties.setProperty("relaxed", value.toString()) } diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt index 1b13f980..8baeda0c 100644 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/MockK.kt @@ -2,7 +2,8 @@ package io.mockk -import io.mockk.impl.restrict.RestrictedMockClasses +import io.mockk.impl.restrict.MockkValidator +import io.mockk.impl.restrict.RestrictMockkConfiguration import kotlin.reflect.KClass /** @@ -38,9 +39,10 @@ inline fun mockk( relaxed: Boolean = false, vararg moreInterfaces: KClass<*>, relaxUnitFun: Boolean = false, - block: T.() -> Unit = {} + mockValidator: MockkValidator = MockkValidator(RestrictMockkConfiguration()), + block: T.() -> Unit = {}, ): T = MockK.useImpl { - RestrictedMockClasses.handleRestrictedMocking(T::class.java) + mockValidator.validateMockableClass(T::class) MockKDsl.internalMockk( name, @@ -470,7 +472,6 @@ inline fun registerInstanceFactory(noinline instanceFactory: ( MockKDsl.internalRegisterInstanceFactory(instanceFactory) } - /** * Executes block of code with registering and unregistering instance factory. */ @@ -709,7 +710,7 @@ inline fun clearAllMocks( objectMocks, staticMocks, constructorMocks, - currentThreadOnly=currentThreadOnly + currentThreadOnly = currentThreadOnly ) } @@ -734,7 +735,6 @@ fun isMockKMock( ) } - object MockKAnnotations { /** * Initializes properties annotated with @MockK, @RelaxedMockK, @Slot and @SpyK in provided object. diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt deleted file mode 100644 index 6f7b96bd..00000000 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/annotations/MockkRestricted.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.mockk.impl.annotations - -import kotlin.reflect.KClass - -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -annotation class MockkRestricted( - val mode: MockkRestrictedMode = MockkRestrictedMode.WARN, - val restricted: Array> = [] -) - -enum class MockkRestrictedMode { - WARN, - EXCEPTION, -} diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt deleted file mode 100644 index bd2fd2d9..00000000 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockingRestrictedExtension.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.mockk.impl.restrict - -import io.mockk.MockKSettings -import io.mockk.impl.annotations.MockkRestricted -import io.mockk.impl.annotations.MockkRestrictedMode -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext -import java.lang.reflect.Method - -class MockingRestrictedExtension : BeforeEachCallback { - - override fun beforeEach(context: ExtensionContext) { - val testMethod: Method? = context.testMethod.orElse(null) - val testClass = context.testClass.orElse(null) - - MockKSettings.setDisallowMockingRestrictedClasses(false) - RestrictedMockClasses.resetUserDefinedRestrictedTypes() - - val annotation = testMethod?.getAnnotation(MockkRestricted::class.java) - ?: testClass?.getAnnotation(MockkRestricted::class.java) - - annotation?.let { - when (it.mode) { - MockkRestrictedMode.EXCEPTION -> MockKSettings.setDisallowMockingRestrictedClasses(true) - MockkRestrictedMode.WARN -> MockKSettings.setDisallowMockingRestrictedClasses(false) - } - - val restrictedClasses = it.restricted.map { clazz -> clazz.java } - RestrictedMockClasses.setUserDefinedRestrictedTypes(restrictedClasses) - } - } -} - diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockkValidator.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockkValidator.kt new file mode 100644 index 00000000..f275d4ae --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/MockkValidator.kt @@ -0,0 +1,35 @@ +package io.mockk.impl.restrict + +import io.mockk.MockKException +import io.mockk.impl.log.Logger +import kotlin.reflect.KClass + +class MockkValidator(private val configuration: RestrictMockkConfiguration) { + private val logger by lazy { Logger() } + private val restrictedClasses: Set = configuration.restrictedTypes + + fun validateMockableClass(clazz: KClass<*>) { + if (isRestrictedClass(clazz)) { + logger.warn { "${clazz.simpleName} should not be mocked! Consider refactoring your test." } + + if (configuration.throwExceptionOnBadMock) { + throw MockKException("Mocking ${clazz.qualifiedName} is not allowed!") + } + } + } + + private fun isRestrictedClass(clazz: KClass<*>): Boolean { + val className = clazz.qualifiedName ?: return false + + if (className in restrictedClasses) return true + + return restrictedClasses.any { restricted -> + try { + val restrictedClass = Class.forName(restricted) + restrictedClass.isAssignableFrom(clazz.java) + } catch (e: ClassNotFoundException) { + false + } + } + } +} diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictMockkConfiguration.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictMockkConfiguration.kt new file mode 100644 index 00000000..22ff970e --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictMockkConfiguration.kt @@ -0,0 +1,41 @@ +package io.mockk.impl.restrict + +import io.mockk.impl.restrict.propertiesloader.DefaultPropertiesLoader +import io.mockk.impl.restrict.propertiesloader.PropertiesLoader +import java.util.* + +class RestrictMockkConfiguration(propertiesLoader: PropertiesLoader = DefaultPropertiesLoader()) { + val userDefinedRestrictedTypes: Set + val restrictedTypes: Set + val throwExceptionOnBadMock: Boolean + + init { + val properties = propertiesLoader.loadProperties() + userDefinedRestrictedTypes = loadRestrictedTypesFromConfig(properties) + + restrictedTypes = DEFAULT_RESTRICTED_CLAZZ + userDefinedRestrictedTypes + throwExceptionOnBadMock = loadThrowExceptionSetting(properties) + } + + companion object { + private val DEFAULT_RESTRICTED_CLAZZ = setOf( + "java.lang.System", + "java.util.Collection", + "java.util.HashMap", + "java.io.File", + "java.nio.file.Path", + ) + + private fun loadRestrictedTypesFromConfig(properties: Properties): Set { + return properties.getProperty("mockk.restrictedClasses", "") + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + } + + private fun loadThrowExceptionSetting(properties: Properties): Boolean { + return properties.getProperty("mockk.throwExceptionOnBadMock", "false").toBoolean() + } + } +} diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt deleted file mode 100644 index 8b9314f9..00000000 --- a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/RestrictedMockClasses.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.mockk.impl.restrict - -import io.mockk.MockKSettings -import java.io.File -import java.nio.file.Path -import java.util.logging.Logger - -object RestrictedMockClasses { - private val logger = Logger.getLogger(RestrictedMockClasses::class.java.name) - - private val defaultRestrictedTypes: MutableSet> = mutableSetOf( - System::class.java, - Collection::class.java, - Map::class.java, - File::class.java, - Path::class.java - ) - - private val userDefinedRestrictedTypes = mutableSetOf>() - - fun handleRestrictedMocking(clazz: Class<*>) { - if (isRestricted(clazz)) { - if (MockKSettings.disallowMockingRestrictedClasses) { - throw IllegalArgumentException("Cannot mock restricted class: ${clazz.name}") - } else { - logger.warning("Warning: Attempting to mock a restricted class (${clazz.name}). This is usually a bad practice.") - } - } - } - - fun isRestricted(clazz: Class<*>): Boolean { - return defaultRestrictedTypes.any { it.isAssignableFrom(clazz) } || - userDefinedRestrictedTypes.any { it.isAssignableFrom(clazz) } - } - - fun setUserDefinedRestrictedTypes(classes: List>) { - userDefinedRestrictedTypes.clear() - userDefinedRestrictedTypes.addAll(classes) - } - - fun resetUserDefinedRestrictedTypes() { - userDefinedRestrictedTypes.clear() - } -} diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/DefaultPropertiesLoader.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/DefaultPropertiesLoader.kt new file mode 100644 index 00000000..9332d622 --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/DefaultPropertiesLoader.kt @@ -0,0 +1,12 @@ +package io.mockk.impl.restrict.propertiesloader + +import java.util.Properties + +class DefaultPropertiesLoader : PropertiesLoader { + override fun loadProperties(): Properties { + val properties = Properties() + val resourceStream = Thread.currentThread().contextClassLoader.getResourceAsStream("mockk.properties") + resourceStream?.use { properties.load(it) } + return properties + } +} diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/PropertiesLoader.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/PropertiesLoader.kt new file mode 100644 index 00000000..54b0bcf2 --- /dev/null +++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/restrict/propertiesloader/PropertiesLoader.kt @@ -0,0 +1,7 @@ +package io.mockk.impl.restrict.propertiesloader + +import java.util.Properties + +interface PropertiesLoader { + fun loadProperties(): Properties +} \ No newline at end of file diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictMockkTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictMockkTest.kt new file mode 100644 index 00000000..f1a1cfc9 --- /dev/null +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictMockkTest.kt @@ -0,0 +1,161 @@ +package io.mockk.restrict + +import io.mockk.MockKException +import io.mockk.impl.restrict.MockkValidator +import io.mockk.impl.restrict.RestrictMockkConfiguration +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.io.File +import java.nio.file.Path +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap + +class RestrictMockkTest { + @Test + fun `when throwExceptionOnBadMock as true should be throw MockException`() { + val config = mapOf("mockk.throwExceptionOnBadMock" to "true") + + assertThrows { + mockk( + mockValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + ) + } + + assertThrows { + mockk( + mockValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + ) + } + + assertThrows { + mockk( + mockValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + ) + } + } + + @Test + fun `when throwExceptionOnBadMock as false should not throw exception`() { + val config = mapOf("mockk.throwExceptionOnBadMock" to "false") + + mockk( + mockValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + ) + } + + @Test + fun `when throwExceptionOnBadMock is true should throw MockException for collections`() { + val config = mapOf( + "mockk.throwExceptionOnBadMock" to "true", + "mockk.restrictedClasses" to "java.util.Collection, java.util.Map" + ) + + val testValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + + assertThrows { mockk>(mockValidator = testValidator) } + assertThrows { mockk>(mockValidator = testValidator) } + assertThrows { mockk>(mockValidator = testValidator) } + } + + @Test + fun `when throwExceptionOnBadMock is false should not throw exception for collections`() { + val config = mapOf( + "mockk.throwExceptionOnBadMock" to "false", + ) + + val testValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + + mockk>(mockValidator = testValidator) + mockk>(mockValidator = testValidator) + mockk>(mockValidator = testValidator) + } + + @Test + fun `when restricted class does not include collections should not throw exception`() { + val config = mapOf( + "mockk.throwExceptionOnBadMock" to "false" + ) + + val testValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + + mockk>(mockValidator = testValidator) + mockk>(mockValidator = testValidator) + } + + @Test + fun `when add custom restricted class should throw exception`() { + val config = mapOf( + "mockk.throwExceptionOnBadMock" to "true", + "mockk.restrictedClasses" to "io.mockk.restrict.Foo" + ) + + val testValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + + assertThrows { + mockk(mockValidator = testValidator) + } + } + + @Test + fun `when add custom restricted classes should throw exception`() { + val config = mapOf( + "mockk.throwExceptionOnBadMock" to "true", + "mockk.restrictedClasses" to "io.mockk.restrict.Foo, io.mockk.restrict.Bar" + ) + + val testValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + + assertThrows { + mockk(mockValidator = testValidator) + } + + assertThrows { + mockk(mockValidator = testValidator) + } + } + + @Test + fun `when add custom restricted sub-class should throw exception`() { + val config = mapOf( + "mockk.throwExceptionOnBadMock" to "true", + "mockk.restrictedClasses" to "io.mockk.restrict.Foo" + ) + + val testValidator = MockkValidator( + RestrictMockkConfiguration(TestPropertiesLoader(config)) + ) + + assertThrows { + mockk(mockValidator = testValidator) + } + + assertDoesNotThrow { + mockk(mockValidator = testValidator) + } + } +} + +open class Foo +class Bar +class FooChild : Foo() \ No newline at end of file diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt deleted file mode 100644 index 057e025c..00000000 --- a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockAtomicTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.mockk.restrict - -import io.mockk.impl.annotations.MockkRestricted -import io.mockk.impl.annotations.MockkRestrictedMode -import io.mockk.impl.restrict.MockingRestrictedExtension -import io.mockk.mockk -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.TestMethodOrder -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import java.io.File -import kotlin.test.Test - -@ExtendWith(MockingRestrictedExtension::class) -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class RestrictedMockAtomicTest { - @Test - @Order(1) - @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION) - fun test1() { - assertThrows { - mockk() - } - } - - @Test - @Order(2) - @MockkRestricted - fun test2() { - mockk() - } - - @Test - @Order(3) - @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION, restricted = [Foo::class]) - fun test3() { - assertThrows{ - mockk() - } - } - - @Test - @Order(4) - fun test4() { - assertDoesNotThrow { - mockk() - } - } - - @Test - @Order(5) - @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION) - fun test5() { - assertThrows { - mockk() - } - } - - @Test - @Order(6) - fun test6() { - assertDoesNotThrow { - mockk() - } - } - - class Foo -} diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt deleted file mode 100644 index c01d79b9..00000000 --- a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/RestrictedMockClassesTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package io.mockk.restrict - -import io.mockk.MockKSettings -import io.mockk.impl.annotations.MockkRestricted -import io.mockk.impl.annotations.MockkRestrictedMode -import io.mockk.impl.restrict.MockingRestrictedExtension -import io.mockk.impl.restrict.RestrictedMockClasses -import io.mockk.mockk -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import java.io.File -import java.nio.file.Path -import java.util.* -import java.util.logging.Level -import java.util.logging.Logger -import java.util.logging.SimpleFormatter -import java.util.logging.StreamHandler -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@ExtendWith(MockingRestrictedExtension::class) -class RestrictedMockClassesTest { - - private fun captureLogs(action: () -> Unit): String { - val logger = Logger.getLogger(RestrictedMockClasses::class.java.name) - val logCapture = StringBuilder() - val handler = object : StreamHandler() { - override fun publish(record: java.util.logging.LogRecord) { - logCapture.append(SimpleFormatter().format(record)) - } - } - - logger.level = Level.WARNING - logger.addHandler(handler) - - action() - - handler.flush() - logger.removeHandler(handler) - return logCapture.toString() - } - - @Test - fun `should detect default restricted types`() { - assertTrue { RestrictedMockClasses.isRestricted(System::class.java) } - - assertTrue { RestrictedMockClasses.isRestricted(ArrayList::class.java) } - assertTrue { RestrictedMockClasses.isRestricted(HashSet::class.java) } - assertTrue { RestrictedMockClasses.isRestricted(TreeMap::class.java) } - assertTrue { RestrictedMockClasses.isRestricted(LinkedList::class.java) } - - assertTrue { RestrictedMockClasses.isRestricted(File::class.java) } - assertTrue { RestrictedMockClasses.isRestricted(Path::class.java) } - } - - @Test - fun `should log warning when attempting to mock restricted class if setting is default`() { - assertDoesNotThrow { mockk() } - } - - @Test - fun `If the annotation is not used, a warning log is recorded`() { - assertDoesNotThrow { mockk() } - } - - @Test - @MockkRestricted(mode = MockkRestrictedMode.WARN) - fun `WARN value of the annotation is a warning log`() { - assertDoesNotThrow { - mockk() - } - } - - @Test - @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION) - fun `If the annotation's option is set, it should throw an IllegalArgumentException`() { - assertThrows { - mockk() - } - } - - @Test - @MockkRestricted(mode = MockkRestrictedMode.EXCEPTION, restricted = [UUID::class, RestrictedMockAtomicTest.Foo::class]) - fun `custom classes can be configured`() { - - assertDoesNotThrow { - mockk() - } - - assertThrows { - mockk() - } - } - - @Test - fun `should throws an exception when attempting to mock restricted class if setting is true`() { - MockKSettings.setDisallowMockingRestrictedClasses(true) - - val ex = assertThrows { - RestrictedMockClasses.handleRestrictedMocking(File::class.java) - } - - assertEquals(ex.message, "Cannot mock restricted class: java.io.File") - } -} diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/TestPropertiesLoader.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/TestPropertiesLoader.kt new file mode 100644 index 00000000..080ae045 --- /dev/null +++ b/modules/mockk/src/commonTest/kotlin/io/mockk/restrict/TestPropertiesLoader.kt @@ -0,0 +1,14 @@ +package io.mockk.restrict + +import io.mockk.impl.restrict.propertiesloader.PropertiesLoader +import java.util.* + +class TestPropertiesLoader(private val mockProperties: Map = emptyMap()) : PropertiesLoader { + override fun loadProperties(): Properties { + val properties = Properties() + mockProperties.forEach { (key, value) -> + properties[key] = value + } + return properties + } +} From a555c75f193553a1ea55eb655aae4f32304f1752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=83=9C=ED=98=84=28TaeHyeon=2C=20Kim=29?= Date: Wed, 5 Feb 2025 23:09:05 +0900 Subject: [PATCH 12/21] add mockk-dsl.api --- modules/mockk-dsl/api/mockk-dsl.api | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/mockk-dsl/api/mockk-dsl.api b/modules/mockk-dsl/api/mockk-dsl.api index 8ee9e3e5..50da55ff 100644 --- a/modules/mockk-dsl/api/mockk-dsl.api +++ b/modules/mockk-dsl/api/mockk-dsl.api @@ -924,13 +924,11 @@ public final class io/mockk/MockKObjectScope : io/mockk/MockKUnmockKScope { public final class io/mockk/MockKSettings { public static final field INSTANCE Lio/mockk/MockKSettings; - public final fun getDisallowMockingRestrictedClasses ()Z public final fun getRecordPrivateCalls ()Z public final fun getRelaxUnitFun ()Z public final fun getRelaxed ()Z public final fun getStackTracesAlignment ()Lio/mockk/StackTracesAlignment; public final fun getStackTracesOnVerify ()Z - public final fun setDisallowMockingRestrictedClasses (Z)V public final fun setRecordPrivateCalls (Z)V public final fun setRelaxUnitFun (Z)V public final fun setRelaxed (Z)V From a9fef76574c6e7e20141bf36e5ee959d3e936e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=83=9C=ED=98=84=28TaeHyeon=2C=20Kim=29?= Date: Sun, 9 Feb 2025 00:31:59 +0900 Subject: [PATCH 13/21] Added doc about how to use the mocking restriction --- README.md | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/README.md b/README.md index f15ea047..42e71cc6 100644 --- a/README.md +++ b/README.md @@ -1495,6 +1495,188 @@ So this is similar to the `returnsMany` semantics. | `position` | the position of an argument in a vararg array | | `nArgs` | overall count of arguments in a vararg array | +## Restricted Mocking in MockK + +### Overview + +**Restricted Mocking** is a feature in MockK designed to **prevent the mocking of classes** that are problematic to mock. +These classes often indicate poor test design and can lead to **unreliable** or **misleading test results**. + +The primary goal is to: +- **Encourage better testing practices** +- **Promote code maintainability** +- **Avoid mocking classes tied to system operations or critical data structures** + + +### Why Restrict Mocking? + +Mocking certain classes can cause several issues: + +| 🚩 **Problem** | ⚠️ **Impact** | +|-----------------------------|-----------------------------------------------------------------------------| +| **False sense of security** | Tests may pass even when the implementation is fragile or incorrect. | +| **Tight coupling** | Tests become overly dependent on low-level implementation details. | +| **Hard-to-maintain tests** | Changes in code can break unrelated tests, increasing maintenance overhead. | +| **Code smells** | Mocking system-level or value-type classes often signals poor architecture. | + + +### Default Restricted Classes + +The following classes are **restricted from being mocked by default**: + +| **Class** | **Description** | **Includes Subtypes?** | +|------------------------|-----------------------------------------------------------------------|------------------------| +| `java.lang.System` | System-related APIs (`System.currentTimeMillis()`, `System.getenv()`) | ✅ Yes | +| `java.util.Collection` | Collections like `List`, `Set`, and `Queue` | ✅ Yes | +| `java.util.Map` | Key-value data structures like `HashMap` | ✅ Yes | +| `java.io.File` | File I/O classes (should be abstracted instead) | ✅ Yes | +| `java.nio.file.Path` | Path manipulation classes for file systems | ✅ Yes | + +⚠️ **Note:** +**All subclasses and implementations** of these classes are also restricted. +For example: +- `ArrayList` and `HashSet` (subtypes of `Collection`) +- `HashMap` (subtype of `Map`) +- Custom classes that extend `File` or implement `Path` + +### How to Configure Restricted Mocking + +You can configure Restricted Mocking behavior using the `mockk.properties` file. + +#### 1. Creating the `mockk.properties` File + +Place the file in one of the following directories: + +```plaintext +src/main/resources/mockk.properties +``` + +#### 2. Configuration Options + +``` +# List of restricted classes (fully qualified names, separated by commas) +restrictedClasses=com.foo.Bar,com.foo.Baz + +# Whether to throw an exception when mocking restricted classes +mockk.throwExceptionOnBadMock=true +``` + +| **Property** | **Description** | **Default Value** | +|---------------------------------|-------------------------------------------------------------------------------------------------|-------------------| +| `mockk.restrictedClasses` | Add fully qualified names of classes to restrict. Supports both system and custom classes. | N/A | +| `mockk.throwExceptionOnBadMock` | `true`: Throws an exception when mocking restricted classes.
`false`: Logs a warning only. | `false` | + +⚠️ **Note:** + +If mockk.throwExceptionOnBadMock is not set, it will default to false, meaning only warnings will be logged. + +To strictly prevent mocking restricted classes, explicitly set: +``` +mockk.throwExceptionOnBadMock=true +``` + +### Behavior When Mocking Restricted Classes + +#### When `mockk.throwExceptionOnBadMock=false` (Default) + +```kotlin +@Test +fun `when throwExceptionOnBadMock is false should not throw exception for collections`() { + val mockList = mockk>() + every { mockList.size } returns 0 + + mockList.size shouldBe 0 +} +``` + +- Result: + - A warning log is generated, but the test passes. + +- Log Example: + - List should not be mocked! Consider refactoring your test. + +#### When `mockk.throwExceptionOnBadMock=true` +```kotlin +@Test +fun `when throwExceptionOnBadMock is true should throw MockKException for collections`() { + assertThrows { + mockk>() // Throws MockException + } +} +``` + +- Result: + - A MockKException is thrown, causing the test to fail. +- Exception Example: + - MockKException: Mocking java.util.HashMap is not allowed! + +### Custom Class Restriction Example + +You can restrict **custom classes** from being mocked using the `mockk.properties` configuration file. +This helps enforce proper testing practices even within your own codebase. + +#### Example 1: Mocking a Restricted Custom Class (Throws Exception) + +Add the following to your `mockk.properties` file: + +```kotlin +package com.foo + +class Foo { + fun doSomething(): String = "print Foo" +} +``` + +```kotlin +package com.bar + +class Bar { + fun doSomething(): String = "print Bar" +} + +class Baz : Bar() { + fun doSomething(): String = "print Baz" +} +``` + +```properties +# Restrict custom classes from being mocked +mockk.restrictedClasses=com.foo.Bar,com.foo.Baz + +# Throw an exception when attempting to mock restricted classes +mockk.throwExceptionOnBadMock=true +``` + +```kotlin +import io.mockk.mockk +import io.mockk.MockKException +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class RestrictedTest { + @Test + fun `should throw exception when mocking restricted class Foo`() { + assertFailsWith { + mockk() // 🚫 This will throw an exception + } + } + + @Test + fun `should throw exception when mocking restricted class Bar`() { + assertFailsWith { + mockk() // 🚫 This will throw an exception + } + } + + @Test + fun `should throw exception when mocking restricted class Baz`() { + assertFailsWith { + mockk() // 🚫 This will throw an exception + } + } +} +``` + ## Funding You can also support this project by becoming a sponsor. Your logo will show up here with a link to your website. From 01207c77f92d52558fe4576a03a6c9fad0f48dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=83=9C=ED=98=84=28TaeHyeon=2C=20Kim=29?= Date: Sun, 9 Feb 2025 00:32:26 +0900 Subject: [PATCH 14/21] re-run ./gradlew :mockk-dsl:apiDump --- modules/mockk/api/mockk.api | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/modules/mockk/api/mockk.api b/modules/mockk/api/mockk.api index 71f42cba..de6f69e6 100644 --- a/modules/mockk/api/mockk.api +++ b/modules/mockk/api/mockk.api @@ -226,18 +226,6 @@ public abstract interface annotation class io/mockk/impl/annotations/MockK : jav public abstract fun relaxed ()Z } -public abstract interface annotation class io/mockk/impl/annotations/MockkRestricted : java/lang/annotation/Annotation { - public abstract fun mode ()Lio/mockk/impl/annotations/MockkRestrictedMode; - public abstract fun restricted ()[Ljava/lang/Class; -} - -public final class io/mockk/impl/annotations/MockkRestrictedMode : java/lang/Enum { - public static final field EXCEPTION Lio/mockk/impl/annotations/MockkRestrictedMode; - public static final field WARN Lio/mockk/impl/annotations/MockkRestrictedMode; - public static fun valueOf (Ljava/lang/String;)Lio/mockk/impl/annotations/MockkRestrictedMode; - public static fun values ()[Lio/mockk/impl/annotations/MockkRestrictedMode; -} - public abstract interface annotation class io/mockk/impl/annotations/OverrideMockKs : java/lang/annotation/Annotation { public abstract fun injectImmutable ()Z public abstract fun lookupType ()Lio/mockk/impl/annotations/InjectionLookupType; @@ -924,17 +912,31 @@ public final class io/mockk/impl/recording/states/VerifyingState$Companion { public final fun getLog ()Lio/mockk/impl/log/Logger; } -public final class io/mockk/impl/restrict/MockingRestrictedExtension : org/junit/jupiter/api/extension/BeforeEachCallback { +public final class io/mockk/impl/restrict/MockkValidator { + public fun (Lio/mockk/impl/restrict/RestrictMockkConfiguration;)V + public final fun validateMockableClass (Lkotlin/reflect/KClass;)V +} + +public final class io/mockk/impl/restrict/RestrictMockkConfiguration { + public static final field Companion Lio/mockk/impl/restrict/RestrictMockkConfiguration$Companion; + public fun ()V + public fun (Lio/mockk/impl/restrict/propertiesloader/PropertiesLoader;)V + public synthetic fun (Lio/mockk/impl/restrict/propertiesloader/PropertiesLoader;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getRestrictedTypes ()Ljava/util/Set; + public final fun getThrowExceptionOnBadMock ()Z + public final fun getUserDefinedRestrictedTypes ()Ljava/util/Set; +} + +public final class io/mockk/impl/restrict/RestrictMockkConfiguration$Companion { +} + +public final class io/mockk/impl/restrict/propertiesloader/DefaultPropertiesLoader : io/mockk/impl/restrict/propertiesloader/PropertiesLoader { public fun ()V - public fun beforeEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V + public fun loadProperties ()Ljava/util/Properties; } -public final class io/mockk/impl/restrict/RestrictedMockClasses { - public static final field INSTANCE Lio/mockk/impl/restrict/RestrictedMockClasses; - public final fun handleRestrictedMocking (Ljava/lang/Class;)V - public final fun isRestricted (Ljava/lang/Class;)Z - public final fun resetUserDefinedRestrictedTypes ()V - public final fun setUserDefinedRestrictedTypes (Ljava/util/List;)V +public abstract interface class io/mockk/impl/restrict/propertiesloader/PropertiesLoader { + public abstract fun loadProperties ()Ljava/util/Properties; } public final class io/mockk/impl/stub/AnswerAnsweringOpportunity : io/mockk/Answer, io/mockk/MockKGateway$AnswerOpportunity { From 335d24ca536a3871f0c9e6dbc890275ae01a95e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=83=9C=ED=98=84=28TaeHyeon=2C=20Kim=29?= Date: Tue, 11 Feb 2025 00:17:12 +0900 Subject: [PATCH 15/21] =?UTF-8?q?typo=20in=20comment=20(MockException=20?= =?UTF-8?q?=E2=86=92=20MockKException)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 42e71cc6..98401ae6 100644 --- a/README.md +++ b/README.md @@ -1600,7 +1600,7 @@ fun `when throwExceptionOnBadMock is false should not throw exception for collec @Test fun `when throwExceptionOnBadMock is true should throw MockKException for collections`() { assertThrows { - mockk>() // Throws MockException + mockk>() // Throws MockKException } } ``` From ad1052482ec52bc764e2c3162c82fbf0ccf4bd02 Mon Sep 17 00:00:00 2001 From: Mattia Tommasone Date: Tue, 11 Feb 2025 14:48:25 +0100 Subject: [PATCH 16/21] Fix broken android.html links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98401ae6..06c3d950 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ testImplementation "io.mockk:mockk:${mockkVersion}" -android Unit +android Unit
 testImplementation "io.mockk:mockk-android:${mockkVersion}"
@@ -55,7 +55,7 @@ testImplementation "io.mockk:mockk-agent:${mockkVersion}"
 
 
 
-android Instrumented
+android Instrumented
 
 
 androidTestImplementation "io.mockk:mockk-android:${mockkVersion}"

From 55cdc4975b7c21fdc5ec07c9b3c333a24856fcf1 Mon Sep 17 00:00:00 2001
From: "Paulo R." 
Date: Tue, 11 Feb 2025 17:15:42 +0000
Subject: [PATCH 17/21] Update README.md - Clarify that private fields cannot
 be mocked

This will avoid the creation of more Issues regarding this topic and save hours to devs trying to mock private fields
---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 06c3d950..a094c351 100644
--- a/README.md
+++ b/README.md
@@ -1123,6 +1123,8 @@ Note: in the examples below, we use `propertyType` to specify the type of the `f
 This is needed because it is possible to capture the type automatically for the getter.
 Use `nullablePropertyType` to specify a nullable type.
 
+**Note:** This is only for public fields. It is nearly impossible to mock private properties as they don't have getter methods attached. Use Java reflection to make the field accessible or use `@VisibleForTesting` annotation in the source.
+
 ```kotlin
 val mock = spyk(MockCls(), recordPrivateCalls = true)
 

From 20a85a3d00eb6e3a10e6ed5ca733da00194e4e41 Mon Sep 17 00:00:00 2001
From: Andreas Wolf 
Date: Mon, 17 Feb 2025 17:00:38 +0100
Subject: [PATCH 18/21] Added new property "failOnSetBackingFieldException" to
 fail test if a backing field could not be set

---
 modules/mockk-dsl/api/mockk-dsl.api           |  2 ++
 .../kotlin/io/mockk/MockKSettings.kt          |  2 ++
 .../jsMain/kotlin/io/mockk/MockKSettings.kt   |  3 ++
 .../jvmMain/kotlin/io/mockk/MockKSettings.kt  |  7 +++++
 .../states/StubbingAwaitingAnswerState.kt     |  7 ++++-
 .../states/StubbingAwaitingAnswerStateTest.kt | 28 +++++++++++++++++++
 6 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/modules/mockk-dsl/api/mockk-dsl.api b/modules/mockk-dsl/api/mockk-dsl.api
index 50da55ff..1c263dec 100644
--- a/modules/mockk-dsl/api/mockk-dsl.api
+++ b/modules/mockk-dsl/api/mockk-dsl.api
@@ -924,11 +924,13 @@ public final class io/mockk/MockKObjectScope : io/mockk/MockKUnmockKScope {
 
 public final class io/mockk/MockKSettings {
 	public static final field INSTANCE Lio/mockk/MockKSettings;
+	public final fun getFailOnSetBackingFieldException ()Z
 	public final fun getRecordPrivateCalls ()Z
 	public final fun getRelaxUnitFun ()Z
 	public final fun getRelaxed ()Z
 	public final fun getStackTracesAlignment ()Lio/mockk/StackTracesAlignment;
 	public final fun getStackTracesOnVerify ()Z
+	public final fun setFailOnSetBackingFieldException (Z)V
 	public final fun setRecordPrivateCalls (Z)V
 	public final fun setRelaxUnitFun (Z)V
 	public final fun setRelaxed (Z)V
diff --git a/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt
index 608ae715..a7b3ebc8 100644
--- a/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt
+++ b/modules/mockk-dsl/src/commonMain/kotlin/io/mockk/MockKSettings.kt
@@ -10,6 +10,8 @@ expect object MockKSettings {
     val stackTracesOnVerify: Boolean
 
     val stackTracesAlignment: StackTracesAlignment
+
+    val failOnSetBackingFieldException: Boolean
 }
 
 enum class StackTracesAlignment {
diff --git a/modules/mockk-dsl/src/jsMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/jsMain/kotlin/io/mockk/MockKSettings.kt
index 283223ca..bf2e0afe 100644
--- a/modules/mockk-dsl/src/jsMain/kotlin/io/mockk/MockKSettings.kt
+++ b/modules/mockk-dsl/src/jsMain/kotlin/io/mockk/MockKSettings.kt
@@ -17,4 +17,7 @@ actual object MockKSettings {
         get() = stackTracesAlignmentValueOf(
             js("global.io_mockk_settings_stackTracesAlignment || \"center\"") as String
         )
+
+    actual val failOnSetBackingFieldException: Boolean
+        get() = js("global.io_mockk_settings_failOnSetBackingFieldException || false") as Boolean
 }
diff --git a/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt b/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt
index 979bdd20..889c4d0f 100644
--- a/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt
+++ b/modules/mockk-dsl/src/jvmMain/kotlin/io/mockk/MockKSettings.kt
@@ -32,6 +32,9 @@ actual object MockKSettings {
     actual val stackTracesAlignment: StackTracesAlignment
         get() = stackTracesAlignmentValueOf(properties.getProperty("stackTracesAlignment", "center"))
 
+    actual val failOnSetBackingFieldException: Boolean
+        get() = booleanProperty("failOnSetBackingFieldException", "false")
+
     fun setRelaxed(value: Boolean) {
         properties.setProperty("relaxed", value.toString())
     }
@@ -47,4 +50,8 @@ actual object MockKSettings {
     fun setStackTracesAlignment(value: String) {
         properties.setProperty("stackTracesAlignment", value)
     }
+
+    fun setFailOnSetBackingFieldException(value: Boolean) {
+        properties.setProperty("failOnSetBackingFieldException", value.toString())
+    }
 }
diff --git a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt
index 6a0cdb9e..ef471e81 100644
--- a/modules/mockk/src/commonMain/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt
+++ b/modules/mockk/src/commonMain/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerState.kt
@@ -67,7 +67,12 @@ class StubbingAwaitingAnswerState(recorder: CommonCallRecorder) : CallRecordingS
 
             InternalPlatformDsl.dynamicSetField(mock, fieldName, ans.constantValue)
         } catch (ex: Exception) {
-            log.warn(ex) { "Failed to set backing field (skipping)" }
+            if (MockKSettings.failOnSetBackingFieldException) {
+                log.error(ex) { "Failed to set backing field"}
+               throw ex
+            } else {
+                log.warn(ex) { "Failed to set backing field (skipping)" }
+            }
         }
     }
 
diff --git a/modules/mockk/src/commonTest/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerStateTest.kt b/modules/mockk/src/commonTest/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerStateTest.kt
index ec3e25ab..a3c10381 100644
--- a/modules/mockk/src/commonTest/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerStateTest.kt
+++ b/modules/mockk/src/commonTest/kotlin/io/mockk/impl/recording/states/StubbingAwaitingAnswerStateTest.kt
@@ -3,6 +3,9 @@ package io.mockk.impl.recording.states
 import io.mockk.*
 import io.mockk.impl.recording.CommonCallRecorder
 import io.mockk.impl.stub.AnswerAnsweringOpportunity
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.assertThrows
+import kotlin.test.AfterTest
 import kotlin.test.BeforeTest
 import kotlin.test.Test
 
@@ -40,4 +43,29 @@ class StubbingAwaitingAnswerStateTest {
         verify { recorder.stubRepo.stubFor(obj2).addAnswer(call2.matcher, ofType(AnswerAnsweringOpportunity::class)) }
         verify { recorder.factories.answeringState(any()) }
     }
+
+    @Test
+    fun `failOnSetBackingFieldException false just runs for invalid mock`() {
+        val testContainerMock = mockk()
+
+        every { testContainerMock getProperty "someInt" } returns "mockValue"
+    }
+
+    @Test
+    fun `failOnSetBackingFieldException true leads to exception for invalid mock`() {
+        try {
+            MockKSettings.setFailOnSetBackingFieldException(true)
+            val testContainerMock = mockk()
+            assertThrows {
+                every { testContainerMock getProperty "someInt" } returns "mockValue" }
+        } finally {
+            // We reset the settings in the end to avoid side effects for other tests
+            MockKSettings.setFailOnSetBackingFieldException(false)
+        }
+    }
+}
+
+private class TestContainer {
+    @Suppress("unused") // Accessed via reflection
+    val someInt = 5
 }
\ No newline at end of file

From 280047bf82b65cbb7fb1010a4320df37a4f2efea Mon Sep 17 00:00:00 2001
From: Andreas Wolf 
Date: Mon, 24 Feb 2025 16:26:57 +0100
Subject: [PATCH 19/21] Added documentation

---
 README.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index a094c351..2419c53e 100644
--- a/README.md
+++ b/README.md
@@ -1309,10 +1309,14 @@ relaxUnitFun=true|false
 recordPrivateCalls=true|false
 stackTracesOnVerify=true|false
 stackTracesAlignment=left|center
+failOnSetBackingFieldException=true|false
 ```
 
-`stackTracesAlignment` determines whether to align the stack traces to the center (default),
+* `stackTracesAlignment` determines whether to align the stack traces to the center (default),
  or to the left (more consistent with usual JVM stackTraces).
+* If `failOnSetBackingFieldException` (`false` by default) is set to `true`, tests fail if a
+ backing field could not be set. Otherwise, only the warning "Failed to set backing field" will be logged.
+ See [here](https://github.com/mockk/mockk/issues/1291) for more details.
 
 ## DSL tables
 

From 67647017cb469c203bf441c1e7cab404fe4e22b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=B3bert=20Papp?= 
Date: Tue, 25 Feb 2025 11:16:03 +0000
Subject: [PATCH 20/21] Fix compilation error in constructedWith docs

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 2419c53e..99607669 100644
--- a/README.md
+++ b/README.md
@@ -498,7 +498,7 @@ assertEquals(4, MockCls(4).add(7))
 
 verify { 
     constructedWith().add(1)
-    constructedWith("2").add(2)
+    constructedWith(EqMatcher("2")).add(2)
     constructedWith(EqMatcher(4)).add(7)
 }
 ```

From 534b69804485192d19827f4c219bc846eeec6f70 Mon Sep 17 00:00:00 2001
From: Mattia Tommasone 
Date: Tue, 4 Mar 2025 11:37:26 +0100
Subject: [PATCH 21/21] Bump version

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index d8014a36..3c484eaa 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=1.13.16
+version=1.13.18-SNAPSHOT
 # Enable Gradle build cache https://docs.gradle.org/current/userguide/build_cache.html
 org.gradle.caching=true
 org.gradle.configureondemand=false