From 601533761768fe5236ea3ce3bebc364ede2a4839 Mon Sep 17 00:00:00 2001 From: Geoff Hackett Date: Sun, 18 Dec 2022 10:15:08 -0500 Subject: [PATCH] checkin because i think i actually may have gotten this working! --- plugins/mockito-factories/build.gradle.kts | 2 +- plugins/mockk-factories/build.gradle.kts | 22 ++++ .../factories/MockkAutoFactoryPlugins.kt | 39 ++++++ .../factories/reflect/MockkAutoFactory.kt | 64 ++++++++++ .../MockkAutoFactoryAnnotationTest.kt | 32 +++++ .../MockkAutoFactoryClassBuilderTest.kt | 31 +++++ .../mockk/factories/MockkAutoFactoryTest.kt | 111 ++++++++++++++++++ plugins/settings.gradle.kts | 1 + 8 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 plugins/mockk-factories/build.gradle.kts create mode 100644 plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryPlugins.kt create mode 100644 plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/reflect/MockkAutoFactory.kt create mode 100644 plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryAnnotationTest.kt create mode 100644 plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryClassBuilderTest.kt create mode 100644 plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryTest.kt diff --git a/plugins/mockito-factories/build.gradle.kts b/plugins/mockito-factories/build.gradle.kts index 4c8ed68..74ee749 100644 --- a/plugins/mockito-factories/build.gradle.kts +++ b/plugins/mockito-factories/build.gradle.kts @@ -1,4 +1,4 @@ -description = "Mockspresso2 plugins for junit5." +description = "Automatic factory support for mockspresso2 using mockito." plugins { id("config-jvm-deploy") diff --git a/plugins/mockk-factories/build.gradle.kts b/plugins/mockk-factories/build.gradle.kts new file mode 100644 index 0000000..ec4be2d --- /dev/null +++ b/plugins/mockk-factories/build.gradle.kts @@ -0,0 +1,22 @@ +description = "Automatic factory support for mockspresso2 using mockk." + +plugins { + id("config-multi-deploy") +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(project(":api")) + implementation(project(":reflect")) + implementation(libs.mockk.core) + } + } + val commonTest by getting { + dependencies { + implementation(project(":core")) + } + } + } +} diff --git a/plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryPlugins.kt b/plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryPlugins.kt new file mode 100644 index 0000000..12cbe03 --- /dev/null +++ b/plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryPlugins.kt @@ -0,0 +1,39 @@ +package com.episode6.mockspresso2.plugins.mockk.factories + +import com.episode6.mockspresso2.MockspressoBuilder +import com.episode6.mockspresso2.MockspressoProperties +import com.episode6.mockspresso2.api.DynamicObjectMaker +import com.episode6.mockspresso2.plugins.mockk.factories.reflect.autoFactoryMock +import com.episode6.mockspresso2.reflect.asKClass +import com.episode6.mockspresso2.reflect.dependencyKey +import kotlin.reflect.full.hasAnnotation + +/** + * Marks any class encountered with the given [A] annotation as a Factory object. The object will be mocked and each + * method will return a dependency from the underlying Mockspresso instance. + */ +inline fun MockspressoBuilder.autoFactoriesByAnnotation(): MockspressoBuilder = + addDynamicObjectMaker { key, deps -> + when { + key.token.asKClass().hasAnnotation() -> DynamicObjectMaker.Answer.Yes(deps.autoFactoryMock(key)) + else -> DynamicObjectMaker.Answer.No + } + } + +/* + * Mark type [T] (with optional [qualifier]) as a Factory object. The object will be mocked and each method will return + * a dependency from the underlying Mockspresso instance. + */ +inline fun MockspressoBuilder.autoFactory(qualifier: Annotation? = null): MockspressoBuilder { + val key = dependencyKey(qualifier) + return dependency(key) { autoFactoryMock(key) } +} + +/** + * Mark type [T] (with optional [qualifier]) as a Factory object which is also accessible via the returned lazy. + * The object will be mocked and each method will return a dependency from the underlying Mockspresso instance. + */ +inline fun MockspressoProperties.autoFactory(qualifier: Annotation? = null): Lazy { + val key = dependencyKey(qualifier) + return dependency(key) { autoFactoryMock(key) } +} diff --git a/plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/reflect/MockkAutoFactory.kt b/plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/reflect/MockkAutoFactory.kt new file mode 100644 index 0000000..8e1230a --- /dev/null +++ b/plugins/mockk-factories/src/commonMain/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/reflect/MockkAutoFactory.kt @@ -0,0 +1,64 @@ +package com.episode6.mockspresso2.plugins.mockk.factories.reflect + +import com.episode6.mockspresso2.MockspressoBuilder +import com.episode6.mockspresso2.MockspressoProperties +import com.episode6.mockspresso2.api.Dependencies +import com.episode6.mockspresso2.reflect.* +import io.mockk.* +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.full.memberProperties + +/** + * Returns a Factory object for the given [factoryKey]. The object will be mocked + * and each method will return a dependency from the underlying Mockspresso instance. + * + * Generally you shouldn't need to access this method directly, prefer applying with [MockspressoBuilder.autoFactory] + * or [MockspressoProperties.autoFactory] + */ +@Suppress("UNCHECKED_CAST") +fun Dependencies.autoFactoryMock(factoryKey: DependencyKey): T = + (MockK.useImpl { + MockKGateway.implementation().mockFactory.mockk( + mockType = factoryKey.token.asKClass(), + name = "autoFactoryMock:$factoryKey", + relaxed = true, + moreInterfaces = emptyArray(), + relaxUnitFun = true, + ) + } as T).also { factory -> + factoryKey.token.asKClass().memberFunctions + .filter { !it.isSuspend } + .filter { it.returnType != Unit::class } + .forEach { func -> + every { + val params: List = (1 until func.parameterCount()).map { i -> reflectiveAny(func.parameters[i].type) } + func.callWith(*(listOf(factory) + params).toTypedArray()) + } answers { + get(DependencyKey(TypeToken(func.returnType), factoryKey.qualifier)) + } + } + } + +@Suppress("UNCHECKED_CAST") +private fun MockKMatcherScope.reflectiveAny(type: KType): Any = + findCallRecorder().matcher(ConstantMatcher(true), type.classifier as KClass) + +private fun MockKMatcherScope.findCallRecorder(): MockKGateway.CallRecorder { + val func = + MockKMatcherScope::class.memberProperties.find { it.returnType.classifier == MockKGateway.CallRecorder::class } + return func!!.call(this) as MockKGateway.CallRecorder +} +/** + * Returns a mockito default [Answer] for use in a mock of the given [factoryKey]. The answer will resolve the return + * type of the called method at runtime and return a dependency from the mockspresso graph. + */ +//fun Dependencies.mockitoAutoFactoryAnswer(factoryKey: DependencyKey<*>): Answer = Answer { invoc -> +// when (invoc.method.returnType) { +// Void.TYPE -> null +// else -> factoryKey.token +// .resolveJvmType(invoc.method.genericReturnType, invoc.method.declaringClass) +// .let { get(DependencyKey(it, factoryKey.qualifier)) } +// } +//} diff --git a/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryAnnotationTest.kt b/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryAnnotationTest.kt new file mode 100644 index 0000000..7a5f36b --- /dev/null +++ b/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryAnnotationTest.kt @@ -0,0 +1,32 @@ +package com.episode6.mockspresso2.plugins.mockk.factories + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.episode6.mockspresso2.MockspressoBuilder +import com.episode6.mockspresso2.dependency +import com.episode6.mockspresso2.realInstance +import org.junit.jupiter.api.Test + +class MockkAutoFactoryAnnotationTest { + + val mxo = MockspressoBuilder() + .autoFactoriesByAnnotation() + .build() + + val ro by mxo.realInstance() + val dep by mxo.dependency { Dependency() } + + @Test fun testDependencyIsFromMap() { + assertThat(ro.dependency).isEqualTo(dep) + } + + annotation class FactoryAnnotation + class Dependency + @FactoryAnnotation interface DependencyFactory { + fun create(name: String): Dependency + } + + class RealObject(dependencyFactory: DependencyFactory) { + val dependency = dependencyFactory.create("real_name") + } +} diff --git a/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryClassBuilderTest.kt b/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryClassBuilderTest.kt new file mode 100644 index 0000000..2e12908 --- /dev/null +++ b/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryClassBuilderTest.kt @@ -0,0 +1,31 @@ +package com.episode6.mockspresso2.plugins.mockk.factories + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.episode6.mockspresso2.MockspressoBuilder +import com.episode6.mockspresso2.dependency +import com.episode6.mockspresso2.realInstance +import org.junit.jupiter.api.Test + +class MockkAutoFactoryClassBuilderTest { + + val mxo = MockspressoBuilder() + .autoFactory() + .build() + + val ro by mxo.realInstance() + val dep by mxo.dependency { Dependency() } + + @Test fun testDependencyIsFromMap() { + assertThat(ro.dependency).isEqualTo(dep) + } + + class Dependency + interface DependencyFactory { + fun create(name: String): Dependency + } + + class RealObject(dependencyFactory: DependencyFactory) { + val dependency = dependencyFactory.create("real_name") + } +} diff --git a/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryTest.kt b/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryTest.kt new file mode 100644 index 0000000..ca55aca --- /dev/null +++ b/plugins/mockk-factories/src/commonTest/kotlin/com/episode6/mockspresso2/plugins/mockk/factories/MockkAutoFactoryTest.kt @@ -0,0 +1,111 @@ +//package com.episode6.mockspresso2.plugins.mockito.factories +// +//import assertk.assertThat +//import assertk.assertions.containsExactly +//import assertk.assertions.containsOnly +//import assertk.assertions.isEqualTo +//import com.episode6.mockspresso2.api.Dependencies +//import com.episode6.mockspresso2.plugins.mockito.factories.reflect.autoFactoryMock +//import com.episode6.mockspresso2.reflect.dependencyKey +//import org.junit.jupiter.api.Test +//import org.mockito.kotlin.KStubbing +//import org.mockito.kotlin.doReturn +//import org.mockito.kotlin.mock +//import org.mockito.kotlin.stub +// +//class MockkAutoFactoryTest { +// +// private val deps: Dependencies = mock() +// +// private inline fun makeMock(): T = deps.autoFactoryMock(dependencyKey()) +// +// @Test fun testFirstLevel() { +// deps.stub { +// onGetKey() doReturn "string" +// onGetKey() doReturn 2 +// onGetKey>() doReturn listOf("strings") +// } +// val mock: IFace = makeMock() +// +// val a: String = mock.giveA() +// val b: Int = mock.giveB() +// val listA: List = mock.giveListA() +// +// assertThat(a).isEqualTo("string") +// assertThat(b).isEqualTo(2) +// assertThat(listA).containsExactly("strings") +// } +// +// @Test fun testSecondLevel() { +// deps.stub { +// onGetKey() doReturn "string" +// onGetKey() doReturn 2 +// onGetKey>() doReturn listOf(3) +// onGetKey>() doReturn mapOf("key" to 6) +// } +// val mock: IFace2 = makeMock() +// +// val a: Int = mock.giveA() +// val b: String = mock.giveB() +// val listA: List = mock.giveListA() +// val mapXY: Map = mock.giveMapXY() +// +// assertThat(a).isEqualTo(2) +// assertThat(b).isEqualTo("string") +// assertThat(listA).containsExactly(3) +// assertThat(mapXY).containsOnly("key" to 6) +// } +// +// @Test fun testThirdLevel() { +// deps.stub { +// onGetKey() doReturn "string" +// onGetKey() doReturn 2 +// onGetKey>() doReturn listOf(3) +// onGetKey>() doReturn mapOf("key" to 6) +// } +// val mock: ConcreteDef = makeMock() +// +// val a: Int = mock.giveA() +// val b: String = mock.giveB() +// val listA: List = mock.giveListA() +// val mapXY: Map = mock.giveMapXY() +// +// assertThat(a).isEqualTo(2) +// assertThat(b).isEqualTo("string") +// assertThat(listA).containsExactly(3) +// assertThat(mapXY).containsOnly("key" to 6) +// } +// +// @Test fun testFirstLevelReversed() { +// deps.stub { +// onGetKey() doReturn "string" +// onGetKey() doReturn 2L +// onGetKey>() doReturn listOf(4L, 5L) +// } +// val mock: IFaceReverse = makeMock() +// +// val a: Long = mock.giveA() +// val b: String = mock.giveB() +// val listA: List = mock.giveListA() +// +// assertThat(a).isEqualTo(2L) +// assertThat(b).isEqualTo("string") +// assertThat(listA).containsExactly(4L, 5L) +// } +// +// private interface IFace { +// fun giveA(): A +// fun giveB(): B +// fun giveListA(): List +// } +// +// private interface IFace2 : IFace { +// fun giveMapXY(): Map +// } +// +// private interface IFaceReverse : IFace +// +// private interface ConcreteDef : IFace2 +//} +// +//private inline fun KStubbing.onGetKey() = onGeneric { get(dependencyKey()) } diff --git a/plugins/settings.gradle.kts b/plugins/settings.gradle.kts index 25026ec..ebf0ea3 100644 --- a/plugins/settings.gradle.kts +++ b/plugins/settings.gradle.kts @@ -8,6 +8,7 @@ listOf( "mockito", "mockito-factories", "mockk", + "mockk-factories", ).forEach { include("$prefix-$it") project(":$prefix-$it").projectDir = file(it)