Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/mockito-factories/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
description = "Mockspresso2 plugins for junit5."
description = "Automatic factory support for mockspresso2 using mockito."
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

piggyback description fix


plugins {
id("config-jvm-deploy")
Expand Down
22 changes: 22 additions & 0 deletions plugins/mockk-factories/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <reified A : Annotation> MockspressoBuilder.autoFactoriesByAnnotation(): MockspressoBuilder =
addDynamicObjectMaker { key, deps ->
when {
key.token.asKClass().hasAnnotation<A>() -> 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 <reified T : Any?> MockspressoBuilder.autoFactory(qualifier: Annotation? = null): MockspressoBuilder {
val key = dependencyKey<T>(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 <reified T : Any?> MockspressoProperties.autoFactory(qualifier: Annotation? = null): Lazy<T> {
val key = dependencyKey<T>(qualifier)
return dependency(key) { autoFactoryMock(key) }
}
Original file line number Diff line number Diff line change
@@ -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 <T : Any?> Dependencies.autoFactoryMock(factoryKey: DependencyKey<T>): 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<Any?> = (1 until func.parameterCount()).map { i -> reflectiveAny(func.parameters[i].type) }
func.callWith(*(listOf<Any?>(factory) + params).toTypedArray())
} answers {
get(DependencyKey(TypeToken(func.returnType), factoryKey.qualifier))
}
}
}

@Suppress("UNCHECKED_CAST")
private fun MockKMatcherScope.reflectiveAny(type: KType): Any =
findCallRecorder().matcher(ConstantMatcher<Any>(true), type.classifier as KClass<Any>)

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<Any> = Answer<Any> { invoc ->
// when (invoc.method.returnType) {
// Void.TYPE -> null
// else -> factoryKey.token
// .resolveJvmType(invoc.method.genericReturnType, invoc.method.declaringClass)
// .let { get(DependencyKey(it, factoryKey.qualifier)) }
// }
//}
Original file line number Diff line number Diff line change
@@ -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<FactoryAnnotation>()
.build()

val ro by mxo.realInstance<RealObject>()
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")
}
}
Original file line number Diff line number Diff line change
@@ -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<DependencyFactory>()
.build()

val ro by mxo.realInstance<RealObject>()
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")
}
}
Original file line number Diff line number Diff line change
@@ -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 <reified T : Any> makeMock(): T = deps.autoFactoryMock(dependencyKey<T>())
//
// @Test fun testFirstLevel() {
// deps.stub {
// onGetKey<String>() doReturn "string"
// onGetKey<Int>() doReturn 2
// onGetKey<List<String>>() doReturn listOf("strings")
// }
// val mock: IFace<String, Int> = makeMock()
//
// val a: String = mock.giveA()
// val b: Int = mock.giveB()
// val listA: List<String> = mock.giveListA()
//
// assertThat(a).isEqualTo("string")
// assertThat(b).isEqualTo(2)
// assertThat(listA).containsExactly("strings")
// }
//
// @Test fun testSecondLevel() {
// deps.stub {
// onGetKey<String>() doReturn "string"
// onGetKey<Int>() doReturn 2
// onGetKey<List<Int>>() doReturn listOf(3)
// onGetKey<Map<String, Int>>() doReturn mapOf("key" to 6)
// }
// val mock: IFace2<String, Int> = makeMock()
//
// val a: Int = mock.giveA()
// val b: String = mock.giveB()
// val listA: List<Int> = mock.giveListA()
// val mapXY: Map<String, Int> = 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<String>() doReturn "string"
// onGetKey<Int>() doReturn 2
// onGetKey<List<Int>>() doReturn listOf(3)
// onGetKey<Map<String, Int>>() doReturn mapOf("key" to 6)
// }
// val mock: ConcreteDef = makeMock()
//
// val a: Int = mock.giveA()
// val b: String = mock.giveB()
// val listA: List<Int> = mock.giveListA()
// val mapXY: Map<String, Int> = 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<String>() doReturn "string"
// onGetKey<Long>() doReturn 2L
// onGetKey<List<Long>>() doReturn listOf(4L, 5L)
// }
// val mock: IFaceReverse<String> = makeMock()
//
// val a: Long = mock.giveA()
// val b: String = mock.giveB()
// val listA: List<Long> = mock.giveListA()
//
// assertThat(a).isEqualTo(2L)
// assertThat(b).isEqualTo("string")
// assertThat(listA).containsExactly(4L, 5L)
// }
//
// private interface IFace<A, B> {
// fun giveA(): A
// fun giveB(): B
// fun giveListA(): List<A>
// }
//
// private interface IFace2<X, Y> : IFace<Y, X> {
// fun giveMapXY(): Map<X, Y>
// }
//
// private interface IFaceReverse<A> : IFace<Long, A>
//
// private interface ConcreteDef : IFace2<String, Int>
//}
//
//private inline fun <reified T : Any> KStubbing<Dependencies>.onGetKey() = onGeneric { get(dependencyKey<T>()) }
1 change: 1 addition & 0 deletions plugins/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ listOf(
"mockito",
"mockito-factories",
"mockk",
"mockk-factories",
).forEach {
include("$prefix-$it")
project(":$prefix-$it").projectDir = file(it)
Expand Down