("release") {
+ afterEvaluate {
+ from(components["java"])
+ }
+ }
+ }
+}
+
+tasks {
+ test {
+ useJUnitPlatform()
+ jvmArgs(
+ "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED"
+ )
+ }
+}
+
+dependencies {
+ api(projects.modules.mockk)
+ implementation(Deps.Libs.kotlinReflect)
+
+ implementation(Deps.Libs.springBootTest)
+ implementation(Deps.Libs.springTest)
+ implementation(Deps.Libs.springContext)
+
+ testImplementation(Deps.Libs.junit4)
+ testImplementation(Deps.Libs.junitJupiter)
+ testImplementation(Deps.Libs.junitJupiterParams)
+ testImplementation(Deps.Libs.assertj)
+}
diff --git a/modules/springmockk/src/main/java/io/mockk/springmockk/MockkBean.java b/modules/springmockk/src/main/java/io/mockk/springmockk/MockkBean.java
new file mode 100644
index 000000000..ddaca1c29
--- /dev/null
+++ b/modules/springmockk/src/main/java/io/mockk/springmockk/MockkBean.java
@@ -0,0 +1,143 @@
+package io.mockk.springmockk;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that can be used to add MockK mocks to a Spring {@link ApplicationContext}. Can be
+ * used as a class level annotation or on fields in either {@code @Configuration} classes,
+ * or test classes that are run with the {@link SpringRunner}.
+ *
+ * Mocks can be registered by type or by {@link #name() bean name}. When registered by
+ * type, any existing single bean of a matching type (including subclasses) in the context
+ * will be replaced by the mock. When registered by name, an existing bean can be
+ * specifically targeted for replacement by a mock. In either case, if no existing bean is
+ * defined a new one will be added. Dependencies that are known to the application context
+ * but are not beans (such as those
+ * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
+ * registered directly}) will not be found and a mocked bean will be added to the context
+ * alongside the existing dependency.
+ *
+ * When {@code @MockkBean} is used on a field, as well as being registered in the
+ * application context, the mock will also be injected into the field. Typical usage might
+ * be:
+ * @RunWith(SpringRunner.class)
+ * class ExampleTests {
+ *
+ * @MockkBean
+ * private lateinit var service: ExampleService
+ *
+ * @Autowired
+ * private lateinit var userOfService: UserOfService
+ *
+ * @Test
+ * void testUserOfService() {
+ * every { service.greet() } returns "Hello"
+ * val actual = userOfService.makeUse()
+ * assertThat(actual).isEqualTo("Was: Hello")
+ * }
+ *
+ * @Configuration
+ * @Import(UserOfService::class) // A @Component injected with ExampleService
+ * class Config {
+ * }
+ *
+ *
+ * }
+ *
If there is more than one bean of the requested type, qualifier metadata must be
+ * specified at field level:
+ * @RunWith(SpringRunner.class)
+ * class ExampleTests {
+ *
+ * @MockkBean
+ * @Qualifier("example")
+ * private lateinit var service: ExampleService
+ *
+ * ...
+ * }
+ *
+ *
+ * This annotation is {@code @Repeatable} and may be specified multiple times when working
+ * with Java 8 or contained within an {@link MockkBeans @MockkBeans} annotation.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see MockkPostProcessor
+ */
+@Target({ElementType.TYPE, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Repeatable(MockkBeans.class)
+public @interface MockkBean {
+
+ /**
+ * The name of the bean to register or replace. If not specified the name will either
+ * be generated or, if the mock replaces an existing bean, the existing name will be
+ * used.
+ *
+ * @return the name of the bean
+ */
+ String name() default "";
+
+ /**
+ * The classes to mock. This is an alias of {@link #classes()} which can be used for
+ * brevity if no other attributes are defined. See {@link #classes()} for details.
+ *
+ * @return the classes to mock
+ */
+ @AliasFor("classes")
+ Class>[] value() default {};
+
+ /**
+ * The classes to mock. Each class specified here will result in a mock being created
+ * and registered with the application context. Classes can be omitted when the
+ * annotation is used on a field.
+ *
+ * When {@code @MockkBean} also defines a {@code name} this attribute can only contain
+ * a single value.
+ *
+ * If this is the only specified attribute consider using the {@code value} alias
+ * instead.
+ *
+ * @return the classes to mock
+ */
+ @AliasFor("value")
+ Class>[] classes() default {};
+
+ /**
+ * Any extra interfaces that should also be declared on the mock.
+ *
+ * @return any extra interfaces
+ */
+ Class>[] extraInterfaces() default {};
+
+ /**
+ * The clear mode to apply to the mock bean. The default is {@link MockkClear#AFTER}
+ * meaning that mocks are automatically reset after each test method is invoked.
+ *
+ * @return the clear mode
+ */
+ MockkClear clear() default MockkClear.AFTER;
+
+ /**
+ * Specifies if the created mock will be relaxed or not
+ *
+ * @return true if relaxed, false otherwise
+ */
+ boolean relaxed() default false;
+
+ /**
+ * Specifies if the created mock will have relaxed Unit
-returning functions
+ *
+ * @return true if relaxed, false otherwise
+ */
+ boolean relaxUnitFun() default false;
+}
diff --git a/modules/springmockk/src/main/java/io/mockk/springmockk/MockkBeans.java b/modules/springmockk/src/main/java/io/mockk/springmockk/MockkBeans.java
new file mode 100644
index 000000000..30fed25f3
--- /dev/null
+++ b/modules/springmockk/src/main/java/io/mockk/springmockk/MockkBeans.java
@@ -0,0 +1,32 @@
+package io.mockk.springmockk;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Container annotation that aggregates several {@link MockkBean} annotations.
+ *
+ * Can be used natively, declaring several nested {@link MockkBean} annotations. Can also
+ * be used in conjunction with Java 8's support for repeatable annotations, where
+ * {@link MockkBean} can simply be declared several times on the same
+ * {@linkplain ElementType#TYPE type}, implicitly generating this container annotation.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+public @interface MockkBeans {
+
+ /**
+ * Return the contained {@link MockkBean} annotations.
+ *
+ * @return the mockk beans
+ */
+ MockkBean[] value();
+
+}
diff --git a/modules/springmockk/src/main/java/io/mockk/springmockk/SpykBean.java b/modules/springmockk/src/main/java/io/mockk/springmockk/SpykBean.java
new file mode 100644
index 000000000..4e2b29377
--- /dev/null
+++ b/modules/springmockk/src/main/java/io/mockk/springmockk/SpykBean.java
@@ -0,0 +1,119 @@
+package io.mockk.springmockk;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that can be used to apply MockK spies to a Spring
+ * {@link ApplicationContext}. Can be used as a class level annotation or on fields in
+ * either {@code @Configuration} classes, or test classes that are
+ * run with the {@link SpringRunner}.
+ *
+ * Spies can be applied by type or by {@link #name() bean name}. All beans in the context
+ * of a matching type (including subclasses) will be wrapped with the spy. If no existing
+ * bean is defined a new one will be added. Dependencies that are known to the application
+ * context but are not beans (such as those
+ * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
+ * registered directly}) will not be found and a spied bean will be added to the context
+ * alongside the existing dependency.
+ *
+ * When {@code @SpykBean} is used on a field, as well as being registered in the
+ * application context, the spy will also be injected into the field. Typical usage might
+ * be:
+ * @RunWith(SpringRunner::class)
+ * class ExampleTests {
+ *
+ * @SpykBean
+ * private lateinit var service: ExampleService;
+ *
+ * @Autowired
+ * private lateinit var userOfService UserOfService;
+ *
+ * @Test
+ * fun testUserOfService() {
+ * val actual = userOfService.makeUse()
+ * assertThat(actual).isEqualTo("Was: Hello")
+ * verify { service.greet() }
+ * }
+ *
+ * @Configuration
+ * @Import(UserOfService::class) // A @Component injected with ExampleService
+ * class Config {
+ * }
+ *
+ *
+ * }
+ *
If there is more than one bean of the requested type, qualifier metadata must be
+ * specified at field level:
+ * @RunWith(SpringRunner.class)
+ * public class ExampleTests {
+ *
+ * @SpykBean
+ * @Qualifier("example")
+ * private lateinit var service: ExampleService
+ *
+ * ...
+ * }
+ *
+ *
+ * This annotation is {@code @Repeatable} and may be specified multiple times when working
+ * with Java 8 or contained within a {@link SpykBeans @SpykBeans} annotation.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see MockkPostProcessor
+ */
+@Target({ElementType.TYPE, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Repeatable(SpykBeans.class)
+public @interface SpykBean {
+
+ /**
+ * The name of the bean to spy. If not specified the name will either be generated or,
+ * if the spy is for an existing bean, the existing name will be used.
+ *
+ * @return the name of the bean
+ */
+ String name() default "";
+
+ /**
+ * The classes to spy. This is an alias of {@link #classes()} which can be used for
+ * brevity if no other attributes are defined. See {@link #classes()} for details.
+ *
+ * @return the classes to spy
+ */
+ @AliasFor("classes")
+ Class>[] value() default {};
+
+ /**
+ * The classes to spy. Each class specified here will result in a spy being applied.
+ * Classes can be omitted when the annotation is used on a field.
+ *
+ * When {@code @SpykBean} also defines a {@code name} this attribute can only contain a
+ * single value.
+ *
+ * If this is the only specified attribute consider using the {@code value} alias
+ * instead.
+ *
+ * @return the classes to spy
+ */
+ @AliasFor("value")
+ Class>[] classes() default {};
+
+ /**
+ * The reset mode to apply to the spied bean. The default is {@link MockkClear#AFTER}
+ * meaning that spies are automatically reset after each test method is invoked.
+ *
+ * @return the reset mode
+ */
+ MockkClear clear() default MockkClear.AFTER;
+}
diff --git a/modules/springmockk/src/main/java/io/mockk/springmockk/SpykBeans.java b/modules/springmockk/src/main/java/io/mockk/springmockk/SpykBeans.java
new file mode 100644
index 000000000..2541d43d4
--- /dev/null
+++ b/modules/springmockk/src/main/java/io/mockk/springmockk/SpykBeans.java
@@ -0,0 +1,30 @@
+package io.mockk.springmockk;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Container annotation that aggregates several {@link SpykBean} annotations.
+ *
+ * Can be used natively, declaring several nested {@link SpykBean} annotations. Can also be
+ * used in conjunction with Java 8's support for repeatable annotations, where
+ * {@link SpykBean} can simply be declared several times on the same
+ * {@linkplain ElementType#TYPE type}, implicitly generating this container annotation.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+public @interface SpykBeans {
+
+ /**
+ * Return the contained {@link SpykBean} annotations.
+ * @return the spy beans
+ */
+ SpykBean[] value();
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/ClearMocksTestExecutionListener.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/ClearMocksTestExecutionListener.kt
new file mode 100644
index 000000000..f71920f18
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/ClearMocksTestExecutionListener.kt
@@ -0,0 +1,78 @@
+package io.mockk.springmockk
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.context.ApplicationContext
+import org.springframework.context.ConfigurableApplicationContext
+import org.springframework.core.Ordered
+import org.springframework.test.context.TestContext
+import org.springframework.test.context.support.AbstractTestExecutionListener
+import org.springframework.util.ClassUtils
+
+/**
+ * `TestExecutionListener` to reset any mock beans that have been marked with a
+ * [MockkClear]. Typically used alongside [MockkTestExecutionListener].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @since 1.4.0
+ * @see MockkTestExecutionListener
+ */
+class ClearMocksTestExecutionListener : AbstractTestExecutionListener() {
+ private val MOCKK_IS_PRESENT = ClassUtils.isPresent(
+ "io.mockk.MockK",
+ ClearMocksTestExecutionListener::class.java.classLoader
+ )
+
+ override fun getOrder(): Int {
+ return Ordered.LOWEST_PRECEDENCE - 100
+ }
+
+ override fun beforeTestMethod(testContext: TestContext) {
+ if (MOCKK_IS_PRESENT) {
+ clearMocks(testContext.applicationContext, MockkClear.BEFORE)
+ }
+ }
+
+ override fun afterTestMethod(testContext: TestContext) {
+ if (MOCKK_IS_PRESENT) {
+ clearMocks(testContext.applicationContext, MockkClear.AFTER)
+ }
+ }
+
+ private fun clearMocks(applicationContext: ApplicationContext, clear: MockkClear) {
+ if (applicationContext is ConfigurableApplicationContext) {
+ clearMocks(applicationContext, clear)
+ }
+ }
+
+ private fun clearMocks(applicationContext: ConfigurableApplicationContext, clear: MockkClear) {
+ val beanFactory = applicationContext.beanFactory
+ val names = beanFactory.beanDefinitionNames
+ val instantiatedSingletons = beanFactory.singletonNames.toSet()
+ for (name in names) {
+ val definition = beanFactory.getBeanDefinition(name)
+ if (definition.isSingleton && instantiatedSingletons.contains(name)) {
+ val bean = beanFactory.getSingleton(name)
+ bean?.let {
+ if (clear == MockkClear.get(bean)) {
+ io.mockk.clearMocks(bean)
+ }
+ }
+ }
+ }
+ try {
+ val mockkCreatedBeans = beanFactory.getBean(MockkCreatedBeans::class.java)
+ for (bean in mockkCreatedBeans) {
+ if (clear == MockkClear.get(bean)) {
+ io.mockk.clearMocks(bean)
+ }
+ }
+ } catch (ex: NoSuchBeanDefinitionException) {
+ // Continue
+ }
+
+ applicationContext.parent ?.let {
+ clearMocks(it, clear)
+ }
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/Definition.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/Definition.kt
new file mode 100644
index 000000000..46e9a678b
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/Definition.kt
@@ -0,0 +1,34 @@
+package io.mockk.springmockk
+
+private const val MULTIPLIER = 31
+
+/**
+ * Base class for [MockkDefinition] and [SpykDefinition].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see DefinitionsParser
+ */
+open class Definition(
+ val name: String?,
+ val clear: MockkClear,
+ val qualifier: QualifierDefinition?
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Definition) return false
+
+ if (name != other.name) return false
+ if (clear != other.clear) return false
+ if (qualifier != other.qualifier) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = name?.hashCode() ?: 0
+ result = MULTIPLIER * result + clear.hashCode()
+ result = MULTIPLIER * result + (qualifier?.hashCode() ?: 0)
+ return result
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/DefinitionsParser.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/DefinitionsParser.kt
new file mode 100644
index 000000000..0c16393aa
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/DefinitionsParser.kt
@@ -0,0 +1,133 @@
+package io.mockk.springmockk
+
+import org.springframework.core.ResolvableType
+import org.springframework.core.annotation.MergedAnnotations
+import org.springframework.core.annotation.MergedAnnotations.SearchStrategy
+import org.springframework.util.Assert
+import org.springframework.util.ReflectionUtils
+import org.springframework.util.StringUtils
+import java.lang.reflect.AnnotatedElement
+import java.lang.reflect.Field
+import java.lang.reflect.TypeVariable
+import java.util.*
+import kotlin.reflect.KClass
+
+
+/**
+ * Parser to create {@link MockkDefinition} and {@link SpykDefinition} instances from
+ * {@link MockkBean @MockkBean} and {@link SpykBean @SpykBean} annotations declared on or in a
+ * class.
+ *
+ * @author Phillip Webb
+ * @author Stephane Nicoll
+ * @author JB Nizet
+ */
+class DefinitionsParser(existing: Collection = emptySet()) {
+ private val definitions = LinkedHashSet()
+ private val definitionFields = mutableMapOf()
+
+ init {
+ definitions.addAll(existing)
+ }
+
+ val parsedDefinitions: Set
+ get() = Collections.unmodifiableSet(definitions)
+
+ fun parse(source: Class<*>) {
+ parseElement(source, null)
+ ReflectionUtils.doWithFields(source) { element -> this.parseElement(element, source) }
+ }
+
+ private fun parseElement(element: AnnotatedElement, source: Class<*>?) {
+ val annotations = MergedAnnotations.from(
+ element,
+ SearchStrategy.SUPERCLASS
+ )
+ annotations.stream(MockkBean::class.java)
+ .map { it.synthesize() }
+ .forEach { parseMockkBeanAnnotation(it, element, source) }
+ annotations.stream(SpykBean::class.java)
+ .map { it.synthesize() }
+ .forEach { parseSpykBeanAnnotation(it, element, source) }
+ }
+
+ private fun parseMockkBeanAnnotation(annotation: MockkBean, element: AnnotatedElement, source: Class<*>?) {
+ val typesToMock = getOrDeduceTypes(element, annotation.value, source)
+ check(!typesToMock.isEmpty()) { "Unable to deduce type to mock from $element" }
+ if (StringUtils.hasLength(annotation.name)) {
+ check(typesToMock.size == 1) { "The name attribute can only be used when mocking a single class" }
+ }
+ for (typeToMock in typesToMock) {
+ val definition = MockkDefinition(
+ name = if (annotation.name.isEmpty()) null else annotation.name,
+ typeToMock = typeToMock,
+ extraInterfaces = annotation.extraInterfaces,
+ clear = annotation.clear,
+ relaxed = annotation.relaxed,
+ relaxUnitFun = annotation.relaxUnitFun,
+ qualifier = QualifierDefinition.forElement(element)
+ )
+ addDefinition(element, definition, "mock")
+ }
+ }
+
+ private fun parseSpykBeanAnnotation(annotation: SpykBean, element: AnnotatedElement, source: Class<*>?) {
+ val typesToSpy = getOrDeduceTypes(element, annotation.value, source)
+ Assert.state(
+ !typesToSpy.isEmpty()
+ ) { "Unable to deduce type to spy from $element" }
+ if (StringUtils.hasLength(annotation.name)) {
+ Assert.state(
+ typesToSpy.size == 1,
+ "The name attribute can only be used when spying a single class"
+ )
+ }
+ for (typeToSpy in typesToSpy) {
+ val definition = SpykDefinition(
+ name = if (annotation.name.isEmpty()) null else annotation.name,
+ typeToSpy = typeToSpy,
+ clear = annotation.clear,
+ qualifier = QualifierDefinition.forElement(element)
+ )
+ addDefinition(element, definition, "spy")
+ }
+ }
+
+ private fun addDefinition(
+ element: AnnotatedElement,
+ definition: Definition,
+ type: String
+ ) {
+ val isNewDefinition = this.definitions.add(definition)
+ Assert.state(
+ isNewDefinition
+ ) { "Duplicate $type definition $definition" }
+ if (element is Field) {
+ this.definitionFields[definition] = element
+ }
+ }
+
+ private fun getOrDeduceTypes(
+ element: AnnotatedElement,
+ value: Array>,
+ source: Class<*>?
+ ): Set {
+ val types = LinkedHashSet()
+ for (clazz in value) {
+ types.add(ResolvableType.forClass(clazz.java))
+ }
+ if (types.isEmpty() && element is Field) {
+ val field = element
+ types.add(if (field.genericType is TypeVariable<*>) {
+ ResolvableType.forField(field, source!!)
+ } else {
+ ResolvableType.forField(field)
+ })
+ }
+ return types
+ }
+
+ fun getField(definition: Definition): Field? {
+ return this.definitionFields[definition]
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkClear.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkClear.kt
new file mode 100644
index 000000000..027780fb2
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkClear.kt
@@ -0,0 +1,67 @@
+package io.mockk.springmockk
+
+import java.lang.ref.WeakReference
+
+/**
+ * Clear strategy used on a mockk bean, applied to a mock via the
+ * [MockkBean] annotation.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @since 1.4.0
+ * @see ClearMocksTestExecutionListener
+ */
+enum class MockkClear {
+ /**
+ * Reset the mock before the test method runs.
+ */
+ BEFORE,
+
+ /**
+ * Reset the mock after the test method runs.
+ */
+ AFTER,
+
+ /**
+ * Don't reset the mock.
+ */
+ NONE;
+
+ companion object {
+ private data class MockkClearEntry(
+ val mockRef: WeakReference,
+ var clearMode: MockkClear
+ )
+
+ // An identity hashmap would be more efficient and was used before.
+ // But it would retain references to mocks that aren't used anymore (because the Spring context cache
+ // has a limit on the number of cached contexts). See https://github.com/Ninja-Squad/springmockk/issues/97
+ // A weak hashmap would be ideal, but it uses equals and hashCode, which would cause hashCode() to be called on the mocks,
+ // and confirmVerified calls to fail. See https://github.com/Ninja-Squad/springmockk/issues/27
+ // and see MockkClearIntegrationTests
+ private val entries = mutableListOf()
+
+ internal fun set(mock: Any, clear: MockkClear) {
+ require(mock.isMock) { "Only mocks can be cleared" }
+ // Using === is important here to not call equals() on the mock.
+ val entry = entries.firstOrNull { it.mockRef.refersTo(mock) }?.apply { clearMode = clear }
+ if (entry == null) {
+ entries.add(MockkClearEntry(WeakReference(mock), clear))
+ }
+ }
+
+ /**
+ * Get the [MockkClear] associated with the given mock.
+ * @param mock the source mock
+ * @return the clear type
+ */
+ fun get(mock: Any): MockkClear {
+ return entries.firstOrNull { it.mockRef.refersTo(mock) }?.clearMode ?: NONE
+ }
+ }
+}
+
+fun T.clear(clear: MockkClear): T {
+ MockkClear.set(this, clear)
+ return this
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkContextCustomizer.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkContextCustomizer.kt
new file mode 100644
index 000000000..efea43f47
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkContextCustomizer.kt
@@ -0,0 +1,27 @@
+package io.mockk.springmockk
+
+import org.springframework.beans.factory.support.BeanDefinitionRegistry
+import org.springframework.context.ConfigurableApplicationContext
+import org.springframework.test.context.ContextCustomizer
+import org.springframework.test.context.MergedContextConfiguration
+
+/**
+ * A {@link ContextCustomizer} to add MockK support.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+data class MockkContextCustomizer(private val definitions: Set) : ContextCustomizer {
+
+ override fun customizeContext(
+ context: ConfigurableApplicationContext,
+ mergedContextConfiguration: MergedContextConfiguration
+ ) {
+ if (context is BeanDefinitionRegistry) {
+ MockkPostProcessor.register(
+ registry = context as BeanDefinitionRegistry,
+ definitions = this.definitions
+ )
+ }
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkContextCustomizerFactory.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkContextCustomizerFactory.kt
new file mode 100644
index 000000000..6ce042037
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkContextCustomizerFactory.kt
@@ -0,0 +1,32 @@
+package io.mockk.springmockk
+
+import org.springframework.test.context.ContextConfigurationAttributes
+import org.springframework.test.context.ContextCustomizer
+import org.springframework.test.context.ContextCustomizerFactory
+import org.springframework.test.context.TestContextAnnotationUtils
+
+/**
+ * A {@link ContextCustomizerFactory} to add MockK support.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class MockkContextCustomizerFactory : ContextCustomizerFactory {
+ override fun createContextCustomizer(
+ testClass: Class<*>,
+ configAttributes: List
+ ): ContextCustomizer {
+ // We gather the explicit mock definitions here since they form part of the
+ // MergedContextConfiguration key. Different mocks need to have a different key.
+ val parser = DefinitionsParser()
+ parseDefinitions(testClass, parser)
+ return MockkContextCustomizer(parser.parsedDefinitions)
+ }
+
+ private fun parseDefinitions(testClass: Class<*>, parser: DefinitionsParser) {
+ parser.parse(testClass)
+ if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
+ parseDefinitions(testClass.enclosingClass, parser)
+ }
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkCreatedBeans.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkCreatedBeans.kt
new file mode 100644
index 000000000..74a8848cd
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkCreatedBeans.kt
@@ -0,0 +1,21 @@
+package io.mockk.springmockk
+
+/**
+ * Beans created using MockK.
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+internal class MockkCreatedBeans : Iterable {
+
+ private val beans = ArrayList()
+
+ fun add(bean: Any) {
+ this.beans.add(bean)
+ }
+
+ override fun iterator(): Iterator {
+ return this.beans.iterator()
+ }
+
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkDefinition.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkDefinition.kt
new file mode 100644
index 000000000..d64d3c72f
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkDefinition.kt
@@ -0,0 +1,74 @@
+package io.mockk.springmockk
+
+import io.mockk.mockkClass
+import org.springframework.core.ResolvableType
+import org.springframework.core.style.ToStringCreator
+import java.util.*
+import kotlin.reflect.KClass
+
+private const val MULTIPLER = 31
+
+/**
+ * A complete definition that can be used to create a MockK mock.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class MockkDefinition(
+ name: String? = null,
+ val typeToMock: ResolvableType,
+ extraInterfaces: Array> = emptyArray(),
+ clear: MockkClear = MockkClear.AFTER,
+ val relaxed: Boolean = false,
+ val relaxUnitFun: Boolean = false,
+ qualifier: QualifierDefinition? = null
+) : Definition(name, clear, qualifier) {
+
+ val extraInterfaces: Set> = Collections.unmodifiableSet(LinkedHashSet(extraInterfaces.toList()))
+
+ fun createMock(): T {
+ return createMock(name)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun createMock(name: String?): T {
+ val resolvedType = typeToMock.resolve()
+ check(resolvedType != null) { "${typeToMock} cannot be resolved" }
+ return mockkClass(
+ type = resolvedType.kotlin as KClass,
+ name = name,
+ moreInterfaces = extraInterfaces.toTypedArray(),
+ relaxed = relaxed,
+ relaxUnitFun = relaxUnitFun
+ ).clear(this.clear)
+ }
+
+ override fun toString(): String {
+ return ToStringCreator(this).append("name", this.name)
+ .append("typeToMock", this.typeToMock)
+ .append("extraInterfaces", this.extraInterfaces)
+ .append("clear", clear).toString()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is MockkDefinition) return false
+ if (!super.equals(other)) return false
+
+ if ((this.typeToMock as Any) != other.typeToMock) return false
+ if (extraInterfaces != other.extraInterfaces) return false
+ if (relaxed != other.relaxed) return false
+ if (relaxUnitFun != other.relaxUnitFun) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = MULTIPLER * result + typeToMock.hashCode()
+ result = MULTIPLER * result + extraInterfaces.hashCode()
+ result = MULTIPLER * result + relaxed.hashCode()
+ result = MULTIPLER * result + relaxUnitFun.hashCode()
+ return result
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkFunctions.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkFunctions.kt
new file mode 100644
index 000000000..5fcc5fcc5
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkFunctions.kt
@@ -0,0 +1,7 @@
+package io.mockk.springmockk
+
+import io.mockk.MockK
+import io.mockk.MockKGateway
+
+val T.isMock: Boolean
+ get() = MockK.useImpl { MockKGateway.implementation().mockFactory.isMock(this) }
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkPostProcessor.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkPostProcessor.kt
new file mode 100644
index 000000000..1dd17b9e7
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkPostProcessor.kt
@@ -0,0 +1,480 @@
+package io.mockk.springmockk
+
+import org.springframework.aop.scope.ScopedProxyUtils
+import org.springframework.beans.BeansException
+import org.springframework.beans.PropertyValues
+import org.springframework.beans.factory.BeanClassLoaderAware
+import org.springframework.beans.factory.BeanCreationException
+import org.springframework.beans.factory.BeanFactory
+import org.springframework.beans.factory.BeanFactoryAware
+import org.springframework.beans.factory.BeanFactoryUtils
+import org.springframework.beans.factory.FactoryBean
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException
+import org.springframework.beans.factory.config.BeanDefinition
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor
+import org.springframework.beans.factory.config.BeanPostProcessor
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
+import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor
+import org.springframework.beans.factory.config.RuntimeBeanReference
+import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor
+import org.springframework.beans.factory.support.BeanDefinitionRegistry
+import org.springframework.beans.factory.support.DefaultBeanNameGenerator
+import org.springframework.beans.factory.support.RootBeanDefinition
+import org.springframework.context.annotation.ConfigurationClassPostProcessor
+import org.springframework.core.Conventions
+import org.springframework.core.Ordered
+import org.springframework.core.PriorityOrdered
+import org.springframework.core.ResolvableType
+import org.springframework.test.context.junit4.SpringRunner
+import org.springframework.util.Assert
+import org.springframework.util.ClassUtils
+import org.springframework.util.ObjectUtils
+import org.springframework.util.ReflectionUtils
+import org.springframework.util.StringUtils
+import java.lang.reflect.Field
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+
+
+
+
+/**
+ * A [BeanFactoryPostProcessor] used to register and inject
+ * [MockkBean](@MockkBeans} with the [ApplicationContext]. An initial set of
+ * definitions can be passed to the processor with additional definitions being
+ * automatically created from `@Configuration` classes that use
+ * [MockkBean](@MockBean).
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @author Andreas Neiser
+ * @author JB Nizet
+ */
+class MockkPostProcessor(private val definitions: Set) : InstantiationAwareBeanPostProcessor,
+ BeanClassLoaderAware, BeanFactoryAware, BeanFactoryPostProcessor, Ordered {
+
+ private val CONFIGURATION_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(
+ ConfigurationClassPostProcessor::class.java,
+ "configurationClass"
+ )
+
+ private var classLoader: ClassLoader? = null
+
+ private lateinit var beanFactory: BeanFactory
+
+ private val mockkCreatedBeans = MockkCreatedBeans()
+
+ private val beanNameRegistry = HashMap()
+
+ private val fieldRegistry = HashMap()
+
+ private val spies = HashMap()
+
+ override fun setBeanClassLoader(classLoader: ClassLoader) {
+ this.classLoader = classLoader
+ }
+
+ @Throws(BeansException::class)
+ override fun setBeanFactory(beanFactory: BeanFactory) {
+ Assert.isInstanceOf(
+ ConfigurableListableBeanFactory::class.java,
+ beanFactory,
+ "Mockk beans can only be used with a ConfigurableListableBeanFactory"
+ )
+ this.beanFactory = beanFactory
+ }
+
+ @Throws(BeansException::class)
+ override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
+ Assert.isInstanceOf(
+ BeanDefinitionRegistry::class.java,
+ beanFactory,
+ "@MockkBean can only be used on bean factories that implement BeanDefinitionRegistry"
+ )
+ postProcessBeanFactory(beanFactory, beanFactory as BeanDefinitionRegistry)
+ }
+
+ private fun postProcessBeanFactory(
+ beanFactory: ConfigurableListableBeanFactory,
+ registry: BeanDefinitionRegistry
+ ) {
+ beanFactory.registerSingleton(MockkBeans::class.java.name, this.mockkCreatedBeans)
+ val parser = DefinitionsParser(this.definitions)
+ for (configurationClass in getConfigurationClasses(beanFactory)) {
+ parser.parse(configurationClass)
+ }
+ val definitions = parser.parsedDefinitions
+ for (definition in definitions) {
+ val field = parser.getField(definition)
+ register(beanFactory, registry, definition, field)
+ }
+ }
+
+ private fun getConfigurationClasses(
+ beanFactory: ConfigurableListableBeanFactory
+ ): Set> {
+ val configurationClasses = LinkedHashSet>()
+ for (beanDefinition in getConfigurationBeanDefinitions(beanFactory).values) {
+ beanDefinition.beanClassName?.let {
+ configurationClasses.add(ClassUtils.resolveClassName(it, this.classLoader))
+ }
+ }
+ return configurationClasses
+ }
+
+ private fun getConfigurationBeanDefinitions(
+ beanFactory: ConfigurableListableBeanFactory
+ ): Map {
+ val definitions = LinkedHashMap()
+ for (beanName in beanFactory.beanDefinitionNames) {
+ val definition = beanFactory.getBeanDefinition(beanName)
+ definition.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE)?.let {
+ definitions[beanName] = definition
+ }
+ }
+ return definitions
+ }
+
+ private fun register(
+ beanFactory: ConfigurableListableBeanFactory,
+ registry: BeanDefinitionRegistry,
+ definition: Definition,
+ field: Field?
+ ) {
+ if (definition is MockkDefinition) {
+ registerMock(beanFactory, registry, definition, field)
+ } else if (definition is SpykDefinition) {
+ registerSpy(beanFactory, registry, definition, field)
+ }
+ }
+
+ private fun registerMock(
+ beanFactory: ConfigurableListableBeanFactory,
+ registry: BeanDefinitionRegistry,
+ definition: MockkDefinition,
+ field: Field?
+ ) {
+ val beanDefinition = createBeanDefinition(definition)
+ val beanName = getBeanName(beanFactory, registry, definition, beanDefinition)
+ val transformedBeanName = BeanFactoryUtils.transformedBeanName(beanName)
+ if (registry.containsBeanDefinition(transformedBeanName)) {
+ val existing = registry.getBeanDefinition(transformedBeanName)
+ copyBeanDefinitionDetails(existing, beanDefinition)
+ registry.removeBeanDefinition(transformedBeanName)
+ }
+ registry.registerBeanDefinition(transformedBeanName, beanDefinition)
+ val mock = definition.createMock("$beanName bean")
+ beanFactory.registerSingleton(transformedBeanName, mock)
+ this.mockkCreatedBeans.add(mock)
+ this.beanNameRegistry[definition] = beanName
+ field?.let {
+ this.fieldRegistry[it] = beanName
+ }
+ }
+
+ private fun createBeanDefinition(mockkDefinition: MockkDefinition): RootBeanDefinition {
+ val definition = RootBeanDefinition(
+ mockkDefinition.typeToMock.resolve()
+ )
+ definition.setTargetType(mockkDefinition.typeToMock)
+ mockkDefinition.qualifier?.applyTo(definition)
+ return definition
+ }
+
+ private fun getBeanName(
+ beanFactory: ConfigurableListableBeanFactory,
+ registry: BeanDefinitionRegistry,
+ mockkDefinition: MockkDefinition,
+ beanDefinition: RootBeanDefinition
+ ): String {
+ if (!mockkDefinition.name.isNullOrEmpty()) {
+ return mockkDefinition.name
+ }
+ val existingBeans = getExistingBeans(beanFactory, mockkDefinition.typeToMock, mockkDefinition.qualifier)
+ if (existingBeans.isEmpty()) {
+ return beanNameGenerator.generateBeanName(beanDefinition, registry)
+ }
+ if (existingBeans.size == 1) {
+ return existingBeans.iterator().next()
+ }
+ val primaryCandidate = determinePrimaryCandidate(registry, existingBeans, mockkDefinition.typeToMock)
+ if (primaryCandidate != null) {
+ return primaryCandidate
+ }
+ throw IllegalStateException(
+ "Unable to register mock bean ${mockkDefinition.typeToMock} expected a single matching bean to replace but found $existingBeans"
+ )
+ }
+
+ private fun copyBeanDefinitionDetails(from: BeanDefinition, to: RootBeanDefinition) {
+ to.isPrimary = from.isPrimary
+ }
+
+ private fun registerSpy(
+ beanFactory: ConfigurableListableBeanFactory,
+ registry: BeanDefinitionRegistry,
+ spykDefinition: SpykDefinition,
+ field: Field?
+ ) {
+ val existingBeans = getExistingBeans(beanFactory, spykDefinition.typeToSpy, spykDefinition.qualifier)
+ if (ObjectUtils.isEmpty(existingBeans)) {
+ createSpy(registry, spykDefinition, field)
+ } else {
+ registerSpies(registry, spykDefinition, field, existingBeans)
+ }
+ }
+
+ private fun getExistingBeans(
+ beanFactory: ConfigurableListableBeanFactory,
+ type: ResolvableType, qualifier: QualifierDefinition?
+ ): Set {
+ val candidates = TreeSet()
+ for (candidate in getExistingBeans(beanFactory, type)) {
+ if (qualifier == null || qualifier.matches(beanFactory, candidate)) {
+ candidates.add(candidate)
+ }
+ }
+ return candidates
+ }
+
+ private fun getExistingBeans(
+ beanFactory: ConfigurableListableBeanFactory,
+ type: ResolvableType
+ ): Set {
+ val beans = LinkedHashSet(beanFactory.getBeanNamesForType(type, true, false).toList())
+ val typeName = type.resolve(Any::class.java).name
+ for (beanName in beanFactory.getBeanNamesForType(FactoryBean::class.java, true, false)) {
+ val transformedBeanName = BeanFactoryUtils.transformedBeanName(beanName)
+ val beanDefinition = beanFactory.getBeanDefinition(transformedBeanName)
+ if (typeName == beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE)) {
+ beans.add(transformedBeanName)
+ }
+ }
+ beans.removeIf { this.isScopedTarget(it) }
+ return beans
+ }
+
+ private fun isScopedTarget(beanName: String): Boolean {
+ try {
+ return ScopedProxyUtils.isScopedTarget(beanName)
+ } catch (ex: Throwable) {
+ return false
+ }
+ }
+
+ private fun createSpy(
+ registry: BeanDefinitionRegistry, spykDefinition: SpykDefinition,
+ field: Field?
+ ) {
+ val beanDefinition = RootBeanDefinition(spykDefinition.typeToSpy.resolve())
+ val beanName = beanNameGenerator.generateBeanName(beanDefinition, registry)
+ registry.registerBeanDefinition(beanName, beanDefinition)
+ registerSpy(spykDefinition, field, beanName)
+ }
+
+ private fun registerSpies(
+ registry: BeanDefinitionRegistry,
+ spykDefinition: SpykDefinition,
+ field: Field?,
+ existingBeans: Collection
+ ) {
+ try {
+ val beanName = determineBeanName(existingBeans, spykDefinition, registry)
+ beanName?.let {
+ registerSpy(spykDefinition, field, it)
+ }
+ } catch (ex: RuntimeException) {
+ throw IllegalStateException("Unable to register spy bean ${spykDefinition.typeToSpy}", ex)
+ }
+ }
+
+ private fun determineBeanName(
+ existingBeans: Collection,
+ definition: SpykDefinition,
+ registry: BeanDefinitionRegistry
+ ): String? {
+ if (StringUtils.hasText(definition.name)) {
+ return definition.name
+ }
+ return if (existingBeans.size == 1) {
+ existingBeans.iterator().next()
+ } else determinePrimaryCandidate(registry, existingBeans, definition.typeToSpy)
+ }
+
+ private fun determinePrimaryCandidate(
+ registry: BeanDefinitionRegistry,
+ candidateBeanNames: Collection,
+ type: ResolvableType
+ ): String? {
+ var primaryBeanName: String? = null
+ for (candidateBeanName in candidateBeanNames) {
+ val beanDefinition = registry.getBeanDefinition(candidateBeanName)
+ if (beanDefinition.isPrimary) {
+ if (primaryBeanName != null) {
+ throw NoUniqueBeanDefinitionException(
+ type.resolve()!!,
+ candidateBeanNames.size,
+ "more than one 'primary' bean found among candidates: $candidateBeanNames"
+ )
+ }
+ primaryBeanName = candidateBeanName
+ }
+ }
+ return primaryBeanName
+ }
+
+ private fun registerSpy(definition: SpykDefinition, field: Field?, beanName: String) {
+ this.spies[beanName] = definition
+ this.beanNameRegistry[definition] = beanName
+ if (field != null) {
+ this.fieldRegistry[field] = beanName
+ }
+ }
+
+ protected fun createSpyIfNecessary(bean: Any, beanName: String): Any {
+ var spy = bean
+ this.spies[beanName]?.let { spy = it.createSpy(beanName, bean) }
+ return spy
+ }
+
+ override fun postProcessProperties(pvs: PropertyValues, bean: Any, beanName: String): PropertyValues {
+ ReflectionUtils.doWithFields(bean.javaClass) { field -> postProcessField(bean, field) }
+ return pvs
+ }
+
+ private fun postProcessField(bean: Any?, field: Field) {
+ val beanName = this.fieldRegistry[field]
+ beanName?.let {
+ if (StringUtils.hasText(it)) {
+ inject(field, bean, it)
+ }
+ }
+ }
+
+ internal fun inject(field: Field, target: Any, definition: Definition) {
+ val beanName = this.beanNameRegistry[definition]
+ check(beanName != null && StringUtils.hasLength(beanName)) { "No bean found for definition $definition" }
+ inject(field, target, beanName)
+ }
+
+ private fun inject(field: Field, target: Any?, beanName: String) {
+ try {
+ field.isAccessible = true
+ val existingValue = ReflectionUtils.getField(field, target)
+ val bean = this.beanFactory.getBean(beanName, field.type)
+ if (existingValue === bean) {
+ return
+ }
+ check(existingValue == null) {
+ "The existing value '${existingValue}' of field '${field}' is not the same as the new value '${bean}'"
+ }
+ ReflectionUtils.setField(field, target, bean)
+ } catch (ex: Throwable) {
+ throw BeanCreationException("Could not inject field: $field", ex)
+ }
+
+ }
+
+ override fun getOrder(): Int {
+ return Ordered.LOWEST_PRECEDENCE - 10
+ }
+
+ /**
+ * [BeanPostProcessor] to handle [SpykBean] definitions. Registered as a
+ * separate processor so that it can be ordered above AOP post processors.
+ */
+ internal class SpyPostProcessor(private val mockkPostProcessor: MockkPostProcessor) :
+ SmartInstantiationAwareBeanPostProcessor,
+ PriorityOrdered {
+
+ private val earlySpyReferences: MutableMap = ConcurrentHashMap(16)
+
+ override fun getOrder(): Int {
+ return Ordered.HIGHEST_PRECEDENCE
+ }
+
+ @Throws(BeansException::class)
+ override fun getEarlyBeanReference(bean: Any, beanName: String): Any {
+ return if (bean is FactoryBean<*>) {
+ bean
+ } else {
+ this.earlySpyReferences.put(getCacheKey(bean, beanName), bean)
+ this.mockkPostProcessor.createSpyIfNecessary(bean, beanName)
+ }
+ }
+
+ @Throws(BeansException::class)
+ override fun postProcessAfterInitialization(bean: Any, beanName: String): Any {
+ return if (bean is FactoryBean<*>) {
+ bean
+ } else if (this.earlySpyReferences.remove(getCacheKey(bean, beanName)) != bean) {
+ this.mockkPostProcessor.createSpyIfNecessary(bean, beanName)
+ } else {
+ bean
+ }
+ }
+
+ private fun getCacheKey(bean: Any, beanName: String): String {
+ return if (StringUtils.hasLength(beanName)) beanName else bean.javaClass.name
+ }
+
+ companion object {
+
+ private val BEAN_NAME = SpyPostProcessor::class.java.name
+
+ fun register(registry: BeanDefinitionRegistry) {
+ if (!registry.containsBeanDefinition(BEAN_NAME)) {
+ val definition = RootBeanDefinition(
+ SpyPostProcessor::class.java
+ )
+ definition.role = BeanDefinition.ROLE_INFRASTRUCTURE
+ val constructorArguments = definition.constructorArgumentValues
+ constructorArguments.addIndexedArgumentValue(0, RuntimeBeanReference(MockkPostProcessor.BEAN_NAME))
+ registry.registerBeanDefinition(BEAN_NAME, definition)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val BEAN_NAME = MockkPostProcessor::class.java.name
+
+ private val beanNameGenerator = DefaultBeanNameGenerator()
+
+ /**
+ * Register the processor with a [BeanDefinitionRegistry]. Not required when
+ * using the [SpringRunner] as registration is automatic.
+ * @param registry the bean definition registry
+ * @param postProcessor the post processor class to register
+ * @param definitions the initial mock/spy definitions
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun register(
+ registry: BeanDefinitionRegistry,
+ postProcessor: Class = MockkPostProcessor::class.java,
+ definitions: Set = emptySet()
+ ) {
+ SpyPostProcessor.register(registry)
+ val definition = getOrAddBeanDefinition(registry, postProcessor)
+ val constructorArg = definition.constructorArgumentValues.getIndexedArgumentValue(0, MutableSet::class.java)
+ val existing = constructorArg!!.value as MutableSet
+ existing.addAll(definitions)
+ }
+
+ private fun getOrAddBeanDefinition(
+ registry: BeanDefinitionRegistry,
+ postProcessor: Class
+ ): BeanDefinition {
+ if (!registry.containsBeanDefinition(BEAN_NAME)) {
+ val definition = RootBeanDefinition(postProcessor)
+ definition.role = BeanDefinition.ROLE_INFRASTRUCTURE
+ val constructorArguments = definition.constructorArgumentValues
+ constructorArguments.addIndexedArgumentValue(0, LinkedHashSet())
+ registry.registerBeanDefinition(BEAN_NAME, definition)
+ return definition
+ }
+ return registry.getBeanDefinition(BEAN_NAME)
+ }
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkTestExecutionListener.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkTestExecutionListener.kt
new file mode 100644
index 000000000..73d57fd7b
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/MockkTestExecutionListener.kt
@@ -0,0 +1,100 @@
+package io.mockk.springmockk
+
+import io.mockk.MockKAnnotations
+import org.springframework.test.context.TestContext
+import org.springframework.test.context.support.AbstractTestExecutionListener
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
+import org.springframework.util.ReflectionUtils
+import java.lang.reflect.Field
+import kotlin.reflect.full.memberProperties
+
+/**
+ * `TestExecutionListener` to enable [@MockkBean][MockkBean] and [@SpykBean][SpykBean] support.
+ * Also triggers [MockKAnnotations#init] when any MockK annotations used.
+ *
+ * To use the automatic reset support of `@MockkBean` and `@SpykBean`, configure
+ * [ClearMocksTestExecutionListener] as well.
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ * @see ClearMocksTestExecutionListener
+ */
+class MockkTestExecutionListener : AbstractTestExecutionListener() {
+ override fun getOrder(): Int {
+ return 1950
+ }
+
+ override fun prepareTestInstance(testContext: TestContext) {
+ initMocks(testContext)
+ injectFields(testContext)
+ }
+
+ override fun beforeTestMethod(testContext: TestContext) {
+ if (testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE) == true) {
+ initMocks(testContext)
+ reinjectFields(testContext)
+ }
+ }
+
+ private fun initMocks(testContext: TestContext) {
+ if (hasMockkAnnotations(testContext)) {
+ MockKAnnotations.init(testContext.testInstance)
+ }
+ }
+
+ private fun hasMockkAnnotations(testContext: TestContext): Boolean {
+ return testContext.testClass.kotlin.memberProperties.any { property ->
+ property.annotations.any {
+ it.annotationClass.java.name.startsWith("io.mockk")
+ }
+ }
+ }
+
+ private fun injectFields(testContext: TestContext) {
+ postProcessFields(testContext) { mockkField, postProcessor ->
+ postProcessor.inject(
+ mockkField.field,
+ mockkField.target,
+ mockkField.definition
+ )
+ }
+ }
+
+ private fun reinjectFields(testContext: TestContext) {
+ postProcessFields(testContext) { mockkField, postProcessor ->
+ ReflectionUtils.makeAccessible(mockkField.field)
+ ReflectionUtils.setField(
+ mockkField.field, testContext.testInstance,
+ null
+ )
+ postProcessor.inject(
+ mockkField.field, mockkField.target,
+ mockkField.definition
+ )
+ }
+ }
+
+ private fun postProcessFields(
+ testContext: TestContext,
+ consumer: (MockkField, MockkPostProcessor) -> Unit
+ ) {
+ val parser = DefinitionsParser()
+ parser.parse(testContext.testClass)
+ if (!parser.parsedDefinitions.isEmpty()) {
+ val postProcessor = testContext.applicationContext.getBean(MockkPostProcessor::class.java)
+ for (definition in parser.parsedDefinitions) {
+ val field = parser.getField(definition)
+ if (field != null) {
+ consumer(MockkField(field, testContext.testInstance, definition), postProcessor)
+ }
+ }
+ }
+ }
+
+ private class MockkField(
+ val field: Field,
+ val target: Any,
+ val definition: Definition
+ )
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/QualifierDefinition.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/QualifierDefinition.kt
new file mode 100644
index 000000000..74cc16472
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/QualifierDefinition.kt
@@ -0,0 +1,82 @@
+package io.mockk.springmockk
+
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
+import org.springframework.beans.factory.config.DependencyDescriptor
+import org.springframework.beans.factory.support.RootBeanDefinition
+import org.springframework.core.annotation.MergedAnnotations
+import java.lang.reflect.AnnotatedElement
+import java.lang.reflect.Field
+
+
+/**
+ * Definition of a Spring [Qualifier](@Qualifier).
+ *
+ * @author Phillip Webb
+ * @author Stephane Nicoll
+ * @author JB Nizet
+ * @see Definition
+ */
+class QualifierDefinition(private val field: Field, private val annotations: Set) {
+
+ private val descriptor: DependencyDescriptor
+
+ init {
+ this.descriptor = DependencyDescriptor(field, true)
+ }
+
+ fun matches(beanFactory: ConfigurableListableBeanFactory, beanName: String): Boolean {
+ return beanFactory.isAutowireCandidate(beanName, this.descriptor)
+ }
+
+ fun applyTo(definition: RootBeanDefinition) {
+ definition.qualifiedElement = this.field
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+ if (other == null || !javaClass.isAssignableFrom(other.javaClass)) {
+ return false
+ }
+ other as QualifierDefinition
+ return this.annotations == other.annotations
+ }
+
+ override fun hashCode(): Int {
+ return this.annotations.hashCode()
+ }
+
+ companion object {
+ fun forElement(element: AnnotatedElement): QualifierDefinition? {
+ if (element is Field) {
+ val annotations = getQualifierAnnotations(element)
+ if (!annotations.isEmpty()) {
+ return QualifierDefinition(element, annotations)
+ }
+ }
+ return null
+ }
+
+ private fun getQualifierAnnotations(field: Field): Set {
+ // Assume that any annotations other than @MockkBean/@SpykBean are qualifiers
+ val candidates = field.declaredAnnotations
+ val annotations = HashSet(candidates.size)
+ for (candidate in candidates) {
+ if (!isMockOrSpyAnnotation(candidate.annotationClass.java)) {
+ annotations.add(candidate)
+ }
+ }
+ return annotations
+ }
+
+ private fun isMockOrSpyAnnotation(type: Class): Boolean {
+ if (type.equals(MockkBean::class.java) || type.equals(SpykBean::class.java)) {
+ return true
+ }
+ val metaAnnotations = MergedAnnotations.from(type)
+ return (metaAnnotations.isPresent(MockkBean::class.java)
+ || metaAnnotations.isPresent(SpykBean::class.java))
+ }
+ }
+}
diff --git a/modules/springmockk/src/main/kotlin/io/mockk/springmockk/SpykDefinition.kt b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/SpykDefinition.kt
new file mode 100644
index 000000000..c6c7fead0
--- /dev/null
+++ b/modules/springmockk/src/main/kotlin/io/mockk/springmockk/SpykDefinition.kt
@@ -0,0 +1,68 @@
+package io.mockk.springmockk
+
+import io.mockk.spyk
+import org.springframework.core.ResolvableType
+import org.springframework.core.style.ToStringCreator
+import org.springframework.util.Assert
+
+private const val MULTIPLIER = 31
+
+/**
+ * A complete definition that can be used to create a MockK spy.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class SpykDefinition(
+ name: String? = null,
+ val typeToSpy: ResolvableType,
+ clear: MockkClear = MockkClear.AFTER,
+ qualifier: QualifierDefinition? = null
+) : Definition(name, clear, qualifier) {
+
+ fun createSpy(instance: T): T {
+ return createSpy(name, instance)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun createSpy(name: String?, instance: T): T {
+ requireNotNull(instance) { "Instance must not be null" }
+ val resolvedType = typeToSpy.resolve()
+ check(resolvedType != null) { "${typeToSpy} cannot be resolved" }
+ Assert.isInstanceOf(resolvedType, instance)
+ if (instance.isMock) {
+ return instance
+ }
+
+ // Spring Boot has a special case for JDK proxies here, introduced in commit
+ // https://github.com/spring-projects/spring-boot/commit/c8c784bd5ca86faaaecdf2371aa35cf98c62efc5#
+ // But the test coming with this commit passed fine with SpringMockK, without introducing any change
+ // and the code used for proxies in Spring Boot wouldn't be usable here anyway, because it relies on a mocked
+ // class with default answers delegating to an instance, but MockK doesn't have such a thing AFAIK.
+
+ return spyk(name = name, objToCopy = instance).clear(this.clear) as T
+ }
+
+ override fun toString(): String {
+ return ToStringCreator(this).append("name", name)
+ .append("typeToSpy", this.typeToSpy)
+ .append("clear", clear)
+ .toString()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SpykDefinition) return false
+ if (!super.equals(other)) return false
+
+ if ((typeToSpy as Any) != other.typeToSpy) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = MULTIPLIER * result + typeToSpy.hashCode()
+ return result
+ }
+}
diff --git a/modules/springmockk/src/main/resources/META-INF/spring.factories b/modules/springmockk/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..c7cab6593
--- /dev/null
+++ b/modules/springmockk/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,8 @@
+# Spring Test ContextCustomizerFactories
+org.springframework.test.context.ContextCustomizerFactory=\
+io.mockk.springmockk.MockkContextCustomizerFactory
+
+# Test Execution Listeners
+org.springframework.test.context.TestExecutionListener=\
+io.mockk.springmockk.MockkTestExecutionListener,\
+io.mockk.springmockk.ClearMocksTestExecutionListener
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/AbstractMockkBeanOnGenericExtensionTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/AbstractMockkBeanOnGenericExtensionTests.kt
new file mode 100644
index 000000000..4dd49b783
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/AbstractMockkBeanOnGenericExtensionTests.kt
@@ -0,0 +1,12 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.AbstractMockkBeanOnGenericTests.SomethingImpl
+import io.mockk.springmockk.AbstractMockkBeanOnGenericTests.ThingImpl
+
+/**
+ * Concrete implementation of [AbstractMockkBeanOnGenericTests].
+ *
+ * @author Madhura Bhave
+ * @author JB Nizet
+ */
+class AbstractMockkBeanOnGenericExtensionTests : AbstractMockkBeanOnGenericTests()
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/AbstractMockkBeanOnGenericTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/AbstractMockkBeanOnGenericTests.kt
new file mode 100644
index 000000000..f50fd8ac4
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/AbstractMockkBeanOnGenericTests.kt
@@ -0,0 +1,52 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.AbstractMockkBeanOnGenericTests.Something
+import io.mockk.springmockk.AbstractMockkBeanOnGenericTests.TestConfiguration
+import io.mockk.springmockk.AbstractMockkBeanOnGenericTests.Thing
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+
+/**
+ * Tests for [MockkBean] with abstract class and generics.
+ *
+ * @author Madhura Bhave
+ * @author JB Nizet
+ */
+@SpringBootTest(classes = [TestConfiguration::class])
+abstract class AbstractMockkBeanOnGenericTests, U : Something> {
+
+ @Autowired
+ private lateinit var thing: T
+
+ @MockkBean
+ private lateinit var something: U
+
+ @Test
+ fun mockkBeanShouldResolveConcreteType() {
+ assertThat(something).isInstanceOf(SomethingImpl::class.java)
+ }
+
+ abstract class Thing {
+ @Autowired
+ lateinit var something: T
+ }
+
+ class SomethingImpl : Something()
+
+ class ThingImpl : Thing()
+
+ open class Something
+
+ @Configuration
+ class TestConfiguration {
+ @Bean
+ fun thing(): ThingImpl {
+ return ThingImpl()
+ }
+ }
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/ClearMocksTestExecutionListenerTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/ClearMocksTestExecutionListenerTests.kt
new file mode 100644
index 000000000..8896d8fdf
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/ClearMocksTestExecutionListenerTests.kt
@@ -0,0 +1,104 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.springmockk.example.ExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.RepeatedTest
+import org.junit.jupiter.api.RepetitionInfo
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.FactoryBean
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Lazy
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Tests for [ClearMocksTestExecutionListener].
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class ClearMocksTestExecutionListenerTests {
+
+ @Autowired
+ private lateinit var context: ApplicationContext
+
+ @RepeatedTest(2)
+ fun test(info: RepetitionInfo) {
+ if (info.currentRepetition == 1) {
+ every { getMock("none").greeting() } returns "none"
+ every { getMock("before").greeting() } returns "before"
+ every { getMock("after").greeting() } returns "after"
+ } else {
+ assertThat(getMock("none").greeting()).isEqualTo("none")
+ assertThat(getMock("before").greeting()).isNotEqualTo("before")
+ assertThat(getMock("after").greeting()).isNotEqualTo("after")
+ }
+ }
+
+ fun getMock(name: String): ExampleService {
+ return this.context.getBean(name, ExampleService::class.java)
+ }
+
+ @Configuration
+ internal class Config {
+
+ @Bean
+ fun before(mockedBeans: MockkCreatedBeans): ExampleService {
+ val mock = mockk(relaxed = true).clear(MockkClear.BEFORE)
+ mockedBeans.add(mock)
+ return mock
+ }
+
+ @Bean
+ fun after(mockedBeans: MockkCreatedBeans): ExampleService {
+ val mock = mockk(relaxed = true).clear(MockkClear.AFTER)
+ mockedBeans.add(mock)
+ return mock
+ }
+
+ @Bean
+ fun none(mockedBeans: MockkCreatedBeans): ExampleService {
+ val mock = mockk(relaxed = true)
+ mockedBeans.add(mock)
+ return mock
+ }
+
+ @Bean
+ @Lazy
+ fun fail(): ExampleService {
+ // gh-5870
+ throw RuntimeException()
+ }
+
+ @Bean
+ fun brokenFactoryBean(): BrokenFactoryBean {
+ // gh-7270
+ return BrokenFactoryBean()
+ }
+
+ }
+
+ internal class BrokenFactoryBean : FactoryBean {
+
+ override fun getObject(): String? {
+ throw IllegalStateException()
+ }
+
+ override fun getObjectType(): Class<*> {
+ return String::class.java
+ }
+
+ override fun isSingleton(): Boolean {
+ return true
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/DefinitionsParserTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/DefinitionsParserTests.kt
new file mode 100644
index 000000000..8e0cdc3e4
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/DefinitionsParserTests.kt
@@ -0,0 +1,260 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleExtraInterface
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.RealExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatIllegalStateException
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.util.ReflectionUtils
+
+/**
+ * Tests for [DefinitionsParser].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class DefinitionsParserTests {
+
+ private val parser = DefinitionsParser()
+
+ private val definitions: List
+ get() = this.parser.parsedDefinitions.toList()
+
+ @Test
+ fun parseSingleMockBean() {
+ this.parser.parse(SingleMockBean::class.java)
+ assertThat(definitions).hasSize(1)
+ val definition = getMockDefinition(0)
+ assertThat(definition.typeToMock.resolve()).isEqualTo(ExampleService::class.java)
+ assertThat(definition.name).isNull()
+ }
+
+ @Test
+ fun parseRepeatMockBean() {
+ this.parser.parse(RepeatMockBean::class.java)
+ assertThat(definitions).hasSize(2)
+ assertThat(getMockDefinition(0).typeToMock.resolve()).isEqualTo(ExampleService::class.java)
+ assertThat(getMockDefinition(1).typeToMock.resolve()).isEqualTo(ExampleServiceCaller::class.java)
+ }
+
+ @Test
+ fun parseMockBeanAttributes() {
+ this.parser.parse(MockBeanAttributes::class.java)
+ assertThat(definitions).hasSize(1)
+ val definition = getMockDefinition(0)
+ assertThat(definition.name).isEqualTo("Name")
+ assertThat(definition.typeToMock.resolve()).isEqualTo(ExampleService::class.java)
+ assertThat(definition.extraInterfaces).containsExactly(ExampleExtraInterface::class)
+ assertThat(definition.relaxed).isTrue()
+ assertThat(definition.relaxUnitFun).isTrue()
+ assertThat(definition.clear).isEqualTo(MockkClear.NONE)
+ assertThat(definition.qualifier).isNull()
+ }
+
+ @Test
+ fun parseMockBeanOnClassAndField() {
+ this.parser.parse(MockBeanOnClassAndField::class.java)
+ assertThat(definitions).hasSize(2)
+ val classDefinition = getMockDefinition(0)
+ assertThat(classDefinition.typeToMock.resolve()).isEqualTo(ExampleService::class.java)
+ assertThat(classDefinition.qualifier).isNull()
+ val fieldDefinition = getMockDefinition(1)
+ assertThat(fieldDefinition.typeToMock.resolve()).isEqualTo(ExampleServiceCaller::class.java)
+ val qualifier = QualifierDefinition.forElement(
+ ReflectionUtils.findField(MockBeanOnClassAndField::class.java, "caller")!!
+ )
+ assertThat(fieldDefinition.qualifier).isNotNull().isEqualTo(qualifier)
+ }
+
+ @Test
+ fun parseMockBeanInferClassToMock() {
+ this.parser.parse(MockBeanInferClassToMock::class.java)
+ assertThat(definitions).hasSize(1)
+ assertThat(getMockDefinition(0).typeToMock.resolve()).isEqualTo(ExampleService::class.java)
+ }
+
+ @Test
+ fun parseMockBeanMissingClassToMock() {
+ assertThatIllegalStateException()
+ .isThrownBy { this.parser.parse(MockBeanMissingClassToMock::class.java) }
+ .withMessageContaining("Unable to deduce type to mock")
+ }
+
+ @Test
+ fun parseMockBeanMultipleClasses() {
+ this.parser.parse(MockBeanMultipleClasses::class.java)
+ assertThat(definitions).hasSize(2)
+ assertThat(getMockDefinition(0).typeToMock.resolve()).isEqualTo(ExampleService::class.java)
+ assertThat(getMockDefinition(1).typeToMock.resolve()).isEqualTo(ExampleServiceCaller::class.java)
+ }
+
+ @Test
+ fun parseMockBeanMultipleClassesWithName() {
+ assertThatIllegalStateException()
+ .isThrownBy { this.parser.parse(MockBeanMultipleClassesWithName::class.java) }
+ .withMessageContaining(
+ "The name attribute can only be used when mocking a single class"
+ )
+ }
+
+ @Test
+ fun parseSingleSpyBean() {
+ this.parser.parse(SingleSpyBean::class.java)
+ assertThat(definitions).hasSize(1)
+ val definition = getSpyDefinition(0)
+ assertThat(definition.typeToSpy.resolve()).isEqualTo(RealExampleService::class.java)
+ assertThat(definition.name).isNull()
+ }
+
+ @Test
+ fun parseRepeatSpyBean() {
+ this.parser.parse(RepeatSpyBean::class.java)
+ assertThat(definitions).hasSize(2)
+ assertThat(getSpyDefinition(0).typeToSpy.resolve()).isEqualTo(RealExampleService::class.java)
+ assertThat(getSpyDefinition(1).typeToSpy.resolve()).isEqualTo(ExampleServiceCaller::class.java)
+ }
+
+ @Test
+ fun parseSpyBeanAttributes() {
+ this.parser.parse(SpyBeanAttributes::class.java)
+ assertThat(definitions).hasSize(1)
+ val definition = getSpyDefinition(0)
+ assertThat(definition.name).isEqualTo("Name")
+ assertThat(definition.typeToSpy.resolve()).isEqualTo(RealExampleService::class.java)
+ assertThat(definition.clear).isEqualTo(MockkClear.NONE)
+ assertThat(definition.qualifier).isNull()
+ }
+
+ @Test
+ fun parseSpyBeanOnClassAndField() {
+ this.parser.parse(SpyBeanOnClassAndField::class.java)
+ assertThat(definitions).hasSize(2)
+ val classDefinition = getSpyDefinition(0)
+ assertThat(classDefinition.qualifier).isNull()
+ assertThat(classDefinition.typeToSpy.resolve()).isEqualTo(RealExampleService::class.java)
+ val fieldDefinition = getSpyDefinition(1)
+ val qualifier = QualifierDefinition.forElement(
+ ReflectionUtils.findField(SpyBeanOnClassAndField::class.java, "caller")!!
+ )
+ assertThat(fieldDefinition.qualifier).isNotNull().isEqualTo(qualifier)
+ assertThat(fieldDefinition.typeToSpy.resolve()).isEqualTo(ExampleServiceCaller::class.java)
+ }
+
+ @Test
+ fun parseSpyBeanInferClassToMock() {
+ this.parser.parse(SpyBeanInferClassToMock::class.java)
+ assertThat(definitions).hasSize(1)
+ assertThat(getSpyDefinition(0).typeToSpy.resolve()).isEqualTo(RealExampleService::class.java)
+ }
+
+ @Test
+ fun parseSpyBeanMissingClassToMock() {
+ assertThatIllegalStateException()
+ .isThrownBy { this.parser.parse(SpyBeanMissingClassToMock::class.java) }
+ .withMessageContaining("Unable to deduce type to spy")
+ }
+
+ @Test
+ fun parseSpyBeanMultipleClasses() {
+ this.parser.parse(SpyBeanMultipleClasses::class.java)
+ assertThat(definitions).hasSize(2)
+ assertThat(getSpyDefinition(0).typeToSpy.resolve()).isEqualTo(RealExampleService::class.java)
+ assertThat(getSpyDefinition(1).typeToSpy.resolve()).isEqualTo(ExampleServiceCaller::class.java)
+ }
+
+ @Test
+ fun parseSpyBeanMultipleClassesWithName() {
+ assertThatIllegalStateException()
+ .isThrownBy { this.parser.parse(SpyBeanMultipleClassesWithName::class.java) }
+ .withMessageContaining(
+ "The name attribute can only be used when spying a single class"
+ )
+ }
+
+ private fun getMockDefinition(index: Int): MockkDefinition {
+ return definitions[index] as MockkDefinition
+ }
+
+ private fun getSpyDefinition(index: Int): SpykDefinition {
+ return definitions[index] as SpykDefinition
+ }
+
+ @MockkBean(ExampleService::class)
+ internal class SingleMockBean
+
+ @MockkBeans(MockkBean(ExampleService::class), MockkBean(ExampleServiceCaller::class))
+ internal class RepeatMockBean
+
+ @MockkBean(
+ name = "Name",
+ classes = [ExampleService::class],
+ extraInterfaces = [ExampleExtraInterface::class],
+ relaxed = true,
+ relaxUnitFun = true,
+ clear = MockkClear.NONE
+ )
+ internal class MockBeanAttributes
+
+ @MockkBean(ExampleService::class)
+ internal class MockBeanOnClassAndField {
+
+ @MockkBean(ExampleServiceCaller::class)
+ @Qualifier("test")
+ private val caller: Any? = null
+
+ }
+
+ @MockkBean(ExampleService::class, ExampleServiceCaller::class)
+ internal class MockBeanMultipleClasses
+
+ @MockkBean(name = "name", classes = [ExampleService::class, ExampleServiceCaller::class])
+ internal class MockBeanMultipleClassesWithName
+
+ internal class MockBeanInferClassToMock {
+
+ @MockkBean
+ private val exampleService: ExampleService? = null
+
+ }
+
+ @MockkBean
+ internal class MockBeanMissingClassToMock
+
+ @SpykBean(RealExampleService::class)
+ internal class SingleSpyBean
+
+ @SpykBeans(SpykBean(RealExampleService::class), SpykBean(ExampleServiceCaller::class))
+ internal class RepeatSpyBean
+
+ @SpykBean(name = "Name", classes = [RealExampleService::class], clear = MockkClear.NONE)
+ internal class SpyBeanAttributes
+
+ @SpykBean(RealExampleService::class)
+ internal class SpyBeanOnClassAndField {
+
+ @SpykBean(ExampleServiceCaller::class)
+ @Qualifier("test")
+ private val caller: Any? = null
+
+ }
+
+ @SpykBean(RealExampleService::class, ExampleServiceCaller::class)
+ internal class SpyBeanMultipleClasses
+
+ @SpykBean(name = "name", classes = arrayOf(RealExampleService::class, ExampleServiceCaller::class))
+ internal class SpyBeanMultipleClassesWithName
+
+ internal class SpyBeanInferClassToMock {
+
+ @SpykBean
+ private val exampleService: RealExampleService? = null
+
+ }
+
+ @SpykBean
+ internal class SpyBeanMissingClassToMock
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/InheritedNestedTestConfigurationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/InheritedNestedTestConfigurationTests.kt
new file mode 100644
index 000000000..30fa29c5c
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/InheritedNestedTestConfigurationTests.kt
@@ -0,0 +1,71 @@
+package io.mockk.springmockk
+
+import io.mockk.verify
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.SpringBootConfiguration
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Import
+import org.springframework.stereotype.Component
+
+/**
+ * Tests for nested test configuration when the configuration is inherited from the
+ * enclosing class (the default behaviour).
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@SpringBootTest(classes = [InheritedNestedTestConfigurationTests.AppConfiguration::class])
+@Import(InheritedNestedTestConfigurationTests.ActionPerformer::class)
+class InheritedNestedTestConfigurationTests {
+ @MockkBean(relaxUnitFun = true)
+ lateinit var action: Action
+
+ @Autowired
+ lateinit var performer: ActionPerformer
+
+ @Test
+ fun mockWasInvokedOnce() {
+ this.performer.run()
+ verify(exactly = 1) { action.perform() }
+ }
+
+ @Test
+ fun mockWasInvokedTwice() {
+ this.performer.run()
+ this.performer.run()
+ verify(exactly = 2) { action.perform() }
+ }
+
+ @Nested
+ inner class InnerTests {
+
+ @Test
+ fun mockWasInvokedOnce() {
+ performer.run()
+ verify(exactly = 1) { action.perform() }
+ }
+
+ @Test
+ fun mockWasInvokedTwice() {
+ performer.run()
+ performer.run()
+ verify(exactly = 2) { action.perform() }
+ }
+ }
+
+ @Component
+ class ActionPerformer(private val action: Action) {
+ fun run() {
+ this.action.perform()
+ }
+ }
+
+ interface Action {
+ fun perform()
+ }
+
+ @SpringBootConfiguration
+ class AppConfiguration
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanContextCachingTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanContextCachingTests.kt
new file mode 100644
index 000000000..573d1225b
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanContextCachingTests.kt
@@ -0,0 +1,102 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.mockk
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.boot.test.context.SpringBootTestContextBootstrapper
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.context.ApplicationContext
+import org.springframework.context.ConfigurableApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.BootstrapContext
+import org.springframework.test.context.MergedContextConfiguration
+import org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate
+import org.springframework.test.context.cache.DefaultContextCache
+import org.springframework.test.util.ReflectionTestUtils
+
+
+/**
+ * Tests for application context caching when using [@MockBean][MockBean].
+ *
+ * @author Andy Wilkinson
+ */
+internal class MockBeanContextCachingTests {
+
+ private val contextCache = DefaultContextCache()
+
+ private val delegate = DefaultCacheAwareContextLoaderDelegate(
+ contextCache
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ @AfterEach
+ fun clearCache() {
+ val contexts = ReflectionTestUtils
+ .getField(
+ contextCache,
+ "contextMap"
+ ) as Map
+ for (context in contexts.values) {
+ if (context is ConfigurableApplicationContext) {
+ context.close()
+ }
+ }
+ contextCache.clear()
+ }
+
+ @Test
+ fun whenThereIsANormalBeanAndAMockBeanThenTwoContextsAreCreated() {
+ bootstrapContext(TestClass::class.java)
+ assertThat(contextCache.size()).isEqualTo(1)
+ bootstrapContext(MockedBeanTestClass::class.java)
+ assertThat(contextCache.size()).isEqualTo(2)
+ }
+
+ @Test
+ fun whenThereIsTheSameMockedBeanInEachTestClassThenOneContextIsCreated() {
+ bootstrapContext(MockedBeanTestClass::class.java)
+ assertThat(contextCache.size()).isEqualTo(1)
+ bootstrapContext(AnotherMockedBeanTestClass::class.java)
+ assertThat(contextCache.size()).isEqualTo(1)
+ }
+
+ private fun bootstrapContext(theTestClass: Class<*>) {
+ val bootstrapper = SpringBootTestContextBootstrapper()
+ val bootstrapContext: BootstrapContext = mockk {
+ every { testClass } returns theTestClass
+ }
+ bootstrapper.bootstrapContext = bootstrapContext
+ every { bootstrapContext.cacheAwareContextLoaderDelegate } returns delegate
+ val testContext = bootstrapper.buildTestContext()
+ testContext.applicationContext
+ }
+
+ @SpringBootTest(classes = [TestConfiguration::class])
+ internal class TestClass
+
+ @SpringBootTest(classes = [TestConfiguration::class])
+ internal class MockedBeanTestClass {
+ @MockkBean
+ private lateinit var testBean: TestBean
+ }
+
+ @SpringBootTest(classes = [TestConfiguration::class])
+ internal class AnotherMockedBeanTestClass {
+ @MockkBean
+ private lateinit var testBean: TestBean
+ }
+
+ @Configuration
+ internal class TestConfiguration {
+ @Bean
+ fun testBean(): TestBean {
+ return TestBean()
+ }
+ }
+
+ internal class TestBean
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanForBeanFactoryIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanForBeanFactoryIntegrationTests.kt
new file mode 100644
index 000000000..358308ab0
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanForBeanFactoryIntegrationTests.kt
@@ -0,0 +1,79 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.mockk
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.FactoryBean
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.getBean
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockBean] for a factory bean.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanForBeanFactoryIntegrationTests {
+
+ // gh-7439
+
+ @MockkBean(relaxed = true)
+ private lateinit var testFactoryBean: TestFactoryBean
+
+ @Autowired
+ private lateinit var applicationContext: ApplicationContext
+
+ @Test
+ fun testName() {
+ val testBean = mockk()
+ every { testBean.hello() } returns "amock"
+
+ every { testFactoryBean.objectType } returns TestBean::class.java as Class<*>
+ every { testFactoryBean.getObject() } returns testBean
+
+ val bean = this.applicationContext.getBean()
+ assertThat(bean.hello()).isEqualTo("amock")
+ }
+
+ @Configuration
+ internal class Config {
+
+ @Bean
+ fun testFactoryBean(): TestFactoryBean {
+ return TestFactoryBean()
+ }
+
+ }
+
+ internal class TestFactoryBean : FactoryBean {
+
+ override fun getObject(): TestBean {
+ return object: TestBean {
+ override fun hello() = "normal"
+ }
+ }
+
+ override fun getObjectType(): Class<*> {
+ return TestBean::class.java
+ }
+
+ override fun isSingleton(): Boolean {
+ return false
+ }
+
+ }
+
+ internal interface TestBean {
+ fun hello(): String
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationClassForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationClassForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..3463dba6e
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationClassForExistingBeanIntegrationTests.kt
@@ -0,0 +1,37 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.FailingExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a configuration class can be used to replace existing beans.
+ *
+ * @author Phillip Webb
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnConfigurationClassForExistingBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { caller.service.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @MockkBean(ExampleService::class)
+ @Import(ExampleServiceCaller::class, FailingExampleService::class)
+ internal class Config
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationClassForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationClassForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..390be2216
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationClassForNewBeanIntegrationTests.kt
@@ -0,0 +1,38 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a configuration class can be used to inject new mock
+ * instances.
+ *
+ * @author Phillip Webb
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnConfigurationClassForNewBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { caller.service.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @MockkBean(ExampleService::class)
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationFieldForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationFieldForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..fdc7711c1
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationFieldForExistingBeanIntegrationTests.kt
@@ -0,0 +1,46 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.FailingExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a field on a `@Configuration` class can be used to
+ * replace existing beans.
+ *
+ * @author Phillip Webb
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnConfigurationFieldForExistingBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var config: Config
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { config.exampleService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class, FailingExampleService::class)
+ internal class Config {
+
+ @MockkBean
+ lateinit var exampleService: ExampleService
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationFieldForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationFieldForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..766c4956b
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnConfigurationFieldForNewBeanIntegrationTests.kt
@@ -0,0 +1,46 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a field on a `@Configuration` class can be used to
+ * inject new mock instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnConfigurationFieldForNewBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var config: Config
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { config.exampleService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config {
+
+ @MockkBean
+ lateinit var exampleService: ExampleService
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnContextHierarchyIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnContextHierarchyIntegrationTests.kt
new file mode 100644
index 000000000..d75e18f4c
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnContextHierarchyIntegrationTests.kt
@@ -0,0 +1,64 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.MockBeanOnContextHierarchyIntegrationTests.ChildConfig
+import io.mockk.springmockk.MockBeanOnContextHierarchyIntegrationTests.ParentConfig
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.getBean
+import org.springframework.beans.factory.getBeanNamesForType
+import org.springframework.context.ApplicationContext
+import org.springframework.context.ApplicationContextAware
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.ContextHierarchy
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] can be used with a [ContextHierarchy].
+ *
+ * @author Phillip Webb
+ */
+@ExtendWith(SpringExtension::class)
+@ContextHierarchy(
+ ContextConfiguration(classes = [ParentConfig::class]),
+ ContextConfiguration(classes = [ChildConfig::class])
+)
+class MockBeanOnContextHierarchyIntegrationTests {
+
+ @Autowired
+ private lateinit var childConfig: ChildConfig
+
+ @Test
+ fun testMocking() {
+ val context = this.childConfig.context
+ val parentContext = context.parent!!
+ assertThat(parentContext.getBeanNamesForType()).hasSize(1)
+ assertThat(parentContext.getBeanNamesForType()).hasSize(0)
+ assertThat(context.getBeanNamesForType()).hasSize(0)
+ assertThat(context.getBeanNamesForType()).hasSize(1)
+ assertThat(context.getBean()).isNotNull()
+ assertThat(context.getBean()).isNotNull()
+ }
+
+ @Configuration
+ @MockkBean(ExampleService::class)
+ internal class ParentConfig
+
+ @Configuration
+ @MockkBean(ExampleServiceCaller::class)
+ internal class ChildConfig : ApplicationContextAware {
+
+ lateinit var context: ApplicationContext
+
+ override fun setApplicationContext(applicationContext: ApplicationContext) {
+ this.context = applicationContext
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnScopedProxyTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnScopedProxyTests.kt
new file mode 100644
index 000000000..a358c0d01
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnScopedProxyTests.kt
@@ -0,0 +1,53 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.FailingExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.context.annotation.Scope
+import org.springframework.context.annotation.ScopedProxyMode
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] when used in combination with scoped proxy targets.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see [gh-5724](https://github.com/spring-projects/spring-boot/issues/5724)
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnScopedProxyTests {
+
+ @MockkBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { caller.service.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config {
+
+ @Bean
+ @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
+ fun exampleService(): ExampleService {
+ return FailingExampleService()
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestClassForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestClassForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..3c74bcc3e
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestClassForExistingBeanIntegrationTests.kt
@@ -0,0 +1,39 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.FailingExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a test class can be used to replace existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@MockkBean(ExampleService::class)
+class MockBeanOnTestClassForExistingBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { caller.service.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class, FailingExampleService::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestClassForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestClassForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..a03e62ba1
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestClassForNewBeanIntegrationTests.kt
@@ -0,0 +1,39 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockBean] on a test class can be used to inject new mock instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@MockkBean(ExampleService::class)
+class MockBeanOnTestClassForNewBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { caller.service.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanCacheIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanCacheIntegrationTests.kt
new file mode 100644
index 000000000..d1490f963
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanCacheIntegrationTests.kt
@@ -0,0 +1,39 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a test class field can be used to replace existing beans when
+ * the context is cached. This test is identical to
+ * [MockBeanOnTestFieldForExistingBeanIntegrationTests] so one of them should
+ * trigger application context caching.
+ *
+ * @author Phillip Webb
+ * @see MockBeanOnTestFieldForExistingBeanIntegrationTests
+ */
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [MockBeanOnTestFieldForExistingBeanConfig::class])
+class MockBeanOnTestFieldForExistingBeanCacheIntegrationTests {
+
+ @MockkBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { exampleService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanConfig.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanConfig.kt
new file mode 100644
index 000000000..13c9e3c48
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanConfig.kt
@@ -0,0 +1,19 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.FailingExampleService
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+
+
+/**
+ * Config for [MockBeanOnTestFieldForExistingBeanIntegrationTests] and
+ * [MockBeanOnTestFieldForExistingBeanCacheIntegrationTests]. Extracted to a shared
+ * config to trigger caching.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@Configuration
+@Import(ExampleServiceCaller::class, FailingExampleService::class)
+class MockBeanOnTestFieldForExistingBeanConfig
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..21892ed0a
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanIntegrationTests.kt
@@ -0,0 +1,37 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a test class field can be used to replace existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see MockBeanOnTestFieldForExistingBeanCacheIntegrationTests
+ */
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [MockBeanOnTestFieldForExistingBeanConfig::class])
+class MockBeanOnTestFieldForExistingBeanIntegrationTests {
+
+ @MockkBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { exampleService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.kt
new file mode 100644
index 000000000..a190067d1
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.kt
@@ -0,0 +1,79 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.CustomQualifier
+import io.mockk.springmockk.example.CustomQualifierExampleService
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.RealExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockBean] on a test class field can be used to replace existing bean while
+ * preserving qualifiers.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests {
+
+ @MockkBean
+ @CustomQualifier
+ private lateinit var service: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Autowired
+ private lateinit var applicationContext: ApplicationContext
+
+ @Test
+ fun testMocking() {
+ every { service.greeting() } returns "Boot"
+ this.caller.sayGreeting()
+ verify { service.greeting() }
+ }
+
+ @Test
+ fun onlyQualifiedBeanIsReplaced() {
+ assertThat(this.applicationContext.getBean("service")).isSameAs(this.service)
+ val anotherService = this.applicationContext.getBean(
+ "anotherService",
+ ExampleService::class.java
+ )
+ assertThat(anotherService.greeting()).isEqualTo("Another")
+ }
+
+ @Configuration
+ internal class TestConfig {
+
+ @Bean
+ fun service(): CustomQualifierExampleService {
+ return CustomQualifierExampleService()
+ }
+
+ @Bean
+ fun anotherService(): ExampleService {
+ return RealExampleService("Another")
+ }
+
+ @Bean
+ fun controller(@CustomQualifier service: ExampleService): ExampleServiceCaller {
+ return ExampleServiceCaller(service)
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..35dc6da4a
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanOnTestFieldForNewBeanIntegrationTests.kt
@@ -0,0 +1,39 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a test class field can be used to inject new mock instances.
+ *
+ * @author Phillip Webb
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanOnTestFieldForNewBeanIntegrationTests {
+
+ @MockkBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { exampleService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithAopProxyTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithAopProxyTests.kt
new file mode 100644
index 000000000..ac1c2115a
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithAopProxyTests.kt
@@ -0,0 +1,78 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.cache.CacheManager
+import org.springframework.cache.annotation.Cacheable
+import org.springframework.cache.annotation.EnableCaching
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager
+import org.springframework.cache.interceptor.CacheResolver
+import org.springframework.cache.interceptor.SimpleCacheResolver
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.stereotype.Service
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] when mixed with Spring AOP.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see [5837](https://github.com/spring-projects/spring-boot/issues/5837)
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanWithAopProxyTests {
+
+ @MockkBean
+ private lateinit var dateService: DateService
+
+ @Test
+ fun verifyShouldUseProxyTarget() {
+ every { dateService.getDate(false) } returns 1L
+ val d1 = this.dateService.getDate(false)
+ assertThat(d1).isEqualTo(1L)
+ every { dateService.getDate(false) } returns 2L
+ val d2 = this.dateService.getDate(false)
+ assertThat(d2).isEqualTo(2L)
+ verify(exactly = 2) { dateService.getDate(false) }
+ verify(exactly = 2) { dateService.getDate(eq(false)) }
+ verify(exactly = 2) { dateService.getDate(any()) }
+ }
+
+ @Configuration
+ @EnableCaching(proxyTargetClass = true)
+ @Import(DateService::class)
+ internal class Config {
+
+ @Bean
+ fun cacheResolver(cacheManager: CacheManager): CacheResolver {
+ val resolver = SimpleCacheResolver()
+ resolver.cacheManager = cacheManager
+ return resolver
+ }
+
+ @Bean
+ fun cacheManager(): ConcurrentMapCacheManager {
+ val cacheManager = ConcurrentMapCacheManager()
+ cacheManager.setCacheNames(listOf("test"))
+ return cacheManager
+ }
+
+ }
+
+ @Service
+ internal class DateService {
+
+ @Cacheable(cacheNames = arrayOf("test"))
+ fun getDate(argument: Boolean): Long {
+ return System.nanoTime()
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithAsyncInterfaceMethodIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithAsyncInterfaceMethodIntegrationTests.kt
new file mode 100644
index 000000000..df251589e
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithAsyncInterfaceMethodIntegrationTests.kt
@@ -0,0 +1,62 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.scheduling.annotation.Async
+import org.springframework.scheduling.annotation.EnableAsync
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Tests for a mock bean where the mocked interface has an async method.
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanWithAsyncInterfaceMethodIntegrationTests {
+
+ @MockkBean
+ private lateinit var transformer: Transformer
+
+ @Autowired
+ private lateinit var service: MyService
+
+ @Test
+ fun mockedMethodsAreNotAsync() {
+ every { transformer.transform("foo") } returns "bar"
+ assertThat(this.service.transform("foo")).isEqualTo("bar")
+ }
+
+ internal interface Transformer {
+
+ @Async
+ fun transform(input: String): String
+
+ }
+
+ internal class MyService(val transformer: Transformer) {
+
+ fun transform(input: String): String {
+ return this.transformer.transform(input)
+ }
+
+ }
+
+ @Configuration
+ @EnableAsync
+ internal class MyConfiguration {
+
+ @Bean
+ fun myService(transformer: Transformer): MyService {
+ return MyService(transformer)
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.kt
new file mode 100644
index 000000000..f25b101fd
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.kt
@@ -0,0 +1,44 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.annotation.DirtiesContext
+import org.springframework.test.annotation.DirtiesContext.ClassMode
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Integration tests for using [MockkBean] with [DirtiesContext] and
+ * [ClassMode.BEFORE_EACH_TEST_METHOD].
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
+class MockBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests {
+
+ @MockkBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { exampleService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say Boot")
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithGenericsOnTestFieldForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithGenericsOnTestFieldForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..e5cc06003
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithGenericsOnTestFieldForNewBeanIntegrationTests.kt
@@ -0,0 +1,44 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleGenericService
+import io.mockk.springmockk.example.ExampleGenericServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockkBean] on a test class field can be used to inject new mock instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanWithGenericsOnTestFieldForNewBeanIntegrationTests {
+
+ @MockkBean
+ private lateinit var exampleIntegerService: ExampleGenericService
+
+ @MockkBean
+ private lateinit var exampleStringService: ExampleGenericService
+
+ @Autowired
+ private lateinit var caller: ExampleGenericServiceCaller
+
+ @Test
+ fun testMocking() {
+ every { exampleIntegerService.greeting() } returns 200
+ every { exampleStringService.greeting() } returns "Boot"
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say 200 Boot")
+ }
+
+ @Configuration
+ @Import(ExampleGenericServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithInjectedFieldIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithInjectedFieldIntegrationTests.kt
new file mode 100644
index 000000000..65371cfab
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithInjectedFieldIntegrationTests.kt
@@ -0,0 +1,44 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Tests for a mock bean where the class being mocked uses field injection.
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockBeanWithInjectedFieldIntegrationTests {
+
+ @MockkBean
+ private lateinit var myService: MyService
+
+ @Test
+ fun fieldInjectionIntoMyServiceMockIsNotAttempted() {
+ every { myService.count() } returns 5
+ assertThat(this.myService.count()).isEqualTo(5)
+ }
+
+ private class MyService {
+
+ @Autowired
+ private lateinit var repository: MyRepository
+
+ fun count() = this.repository.findAll().size
+
+ }
+
+ private interface MyRepository {
+
+ fun findAll(): List
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithRelaxUnitFunIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithRelaxUnitFunIntegrationTests.kt
new file mode 100644
index 000000000..df35d2175
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockBeanWithRelaxUnitFunIntegrationTests.kt
@@ -0,0 +1,44 @@
+package io.mockk.springmockk
+
+import io.mockk.verify
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [MockBean] with `relaxUnitFun`.
+ *
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@MockkBean(UnitReturningService::class, relaxUnitFun = true)
+class MockBeanWithRelaxUnitFunIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: UnitReturningServiceCaller
+
+ @Test
+ fun testMocking() {
+ caller.call("Boot")
+ verify { caller.service.greet("Boot") }
+ }
+
+ @Configuration
+ @Import(UnitReturningServiceCaller::class)
+ internal class Config
+}
+
+interface UnitReturningService {
+ fun greet(message: String): Unit
+}
+
+class UnitReturningServiceCaller(val service: UnitReturningService) {
+ fun call(message: String) {
+ this.service.greet(message)
+ }
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkClearIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkClearIntegrationTests.kt
new file mode 100644
index 000000000..c4937ce05
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkClearIntegrationTests.kt
@@ -0,0 +1,33 @@
+package io.mockk.springmockk
+
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.verify
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+/**
+ * Integration test for [MockkClear]
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class MockkClearIntegrationTests {
+ @MockkBean
+ private lateinit var exampleService: ExampleService
+
+ /**
+ * Test case for Issue #27. It fails if MockkClear uses a HashMap or a ConcurrentHashMap
+ * @see https://github.com/Ninja-Squad/springmockk/issues/27
+ */
+ @Test
+ fun test() {
+ val bean = ExampleServiceCaller(exampleService)
+ every { exampleService.greeting() } returns "test"
+ bean.sayGreeting()
+ verify { exampleService.greeting() }
+ confirmVerified(exampleService) // this is what fails when using a HashMap, because hashCode() is considered not verified
+ }
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkClearTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkClearTests.kt
new file mode 100644
index 000000000..0c886ab71
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkClearTests.kt
@@ -0,0 +1,43 @@
+package io.mockk.springmockk
+
+import io.mockk.mockk
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.RealExampleService
+import io.mockk.spyk
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.jupiter.api.Test
+
+
+/**
+ * Tests for [MockkClear].
+ *
+ * @author JB Nizet
+ */
+class MockkClearTests {
+
+ @Test
+ fun `a simple mock should have NONE as clear` () {
+ val mock = mockk()
+ assertThat(MockkClear.get(mock)).isEqualTo(MockkClear.NONE)
+ }
+
+ @Test
+ fun `a mock cleared with BEFORE has a BEFORE clear`() {
+ val mock = mockk().clear(MockkClear.NONE)
+ assertThat(MockkClear.get(mock)).isEqualTo(MockkClear.NONE)
+ }
+
+ @Test
+ fun `a spy cleared with BEFORE has a BEFORE clear`() {
+ val spy = spyk(RealExampleService("hello")).clear(MockkClear.NONE)
+ assertThat(MockkClear.get(spy)).isEqualTo(MockkClear.NONE)
+ }
+
+ @Test
+ fun `only mocks can be cleared`() {
+ assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
+ RealExampleService("hello").clear(MockkClear.NONE)
+ }
+ }
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkContextCustomizerFactoryTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkContextCustomizerFactoryTests.kt
new file mode 100644
index 000000000..449115408
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkContextCustomizerFactoryTests.kt
@@ -0,0 +1,66 @@
+package io.mockk.springmockk
+
+import io.mockk.MockKAnnotations
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+
+/**
+ * Tests for [MockkContextCustomizerFactory].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class MockkContextCustomizerFactoryTests {
+
+ private val factory = MockkContextCustomizerFactory()
+
+ @BeforeEach
+ fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+ @Test
+ fun getContextCustomizerWithoutAnnotationReturnsCustomizer() {
+ val customizer = this.factory.createContextCustomizer(NoMockBeanAnnotation::class.java, emptyList())
+ assertThat(customizer).isNotNull()
+ }
+
+ @Test
+ fun getContextCustomizerWithAnnotationReturnsCustomizer() {
+ val customizer = this.factory.createContextCustomizer(WithMockBeanAnnotation::class.java, emptyList())
+ assertThat(customizer).isNotNull()
+ }
+
+ @Test
+ fun getContextCustomizerUsesMocksAsCacheKey() {
+ val customizer = this.factory.createContextCustomizer(WithMockBeanAnnotation::class.java, emptyList())
+ assertThat(customizer).isNotNull()
+ val same = this.factory.createContextCustomizer(WithSameMockBeanAnnotation::class.java, emptyList())
+ assertThat(customizer).isNotNull()
+ val different = this.factory.createContextCustomizer(WithDifferentMockBeanAnnotation::class.java, emptyList())
+ assertThat(different).isNotNull()
+ assertThat(customizer.hashCode()).isEqualTo(same.hashCode())
+ assertThat(customizer.hashCode()).isNotEqualTo(different.hashCode())
+ assertThat(customizer).isEqualTo(customizer)
+ assertThat(customizer).isEqualTo(same)
+ assertThat(customizer).isNotEqualTo(different)
+ }
+
+ internal class NoMockBeanAnnotation
+
+ @MockkBean(Service1::class, Service2::class)
+ internal class WithMockBeanAnnotation
+
+ @MockkBean(Service2::class, Service1::class)
+ internal class WithSameMockBeanAnnotation
+
+ @MockkBean(Service1::class)
+ internal class WithDifferentMockBeanAnnotation
+
+ internal interface Service1
+
+ internal interface Service2
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkContextCustomizerTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkContextCustomizerTests.kt
new file mode 100644
index 000000000..59fcebacf
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkContextCustomizerTests.kt
@@ -0,0 +1,35 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.springframework.core.ResolvableType
+import java.util.Collections.emptySet
+
+
+/**
+ * Tests for [MockkContextCustomizer].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class MockkContextCustomizerTests {
+
+ @Test
+ fun hashCodeAndEquals() {
+ val d1 = createTestMockDefinition(ExampleService::class.java)
+ val d2 = createTestMockDefinition(ExampleServiceCaller::class.java)
+ val c1 = MockkContextCustomizer(emptySet())
+ val c2 = MockkContextCustomizer(LinkedHashSet(listOf(d1, d2)))
+ val c3 = MockkContextCustomizer(LinkedHashSet(listOf(d2, d1)))
+ assertThat(c2.hashCode()).isEqualTo(c3.hashCode())
+ assertThat(c1).isEqualTo(c1).isNotEqualTo(c2)
+ assertThat(c2).isEqualTo(c2).isEqualTo(c3).isNotEqualTo(c1)
+ }
+
+ private fun createTestMockDefinition(typeToMock: Class<*>): MockkDefinition {
+ return MockkDefinition(typeToMock = ResolvableType.forClass(typeToMock))
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkDefinitionTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkDefinitionTests.kt
new file mode 100644
index 000000000..75d048e69
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkDefinitionTests.kt
@@ -0,0 +1,99 @@
+package io.mockk.springmockk
+
+import io.mockk.MockKException
+import io.mockk.mockk
+import io.mockk.springmockk.example.ExampleExtraInterface
+import io.mockk.springmockk.example.ExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatCode
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.jupiter.api.Test
+import org.springframework.core.ResolvableType
+
+
+/**
+ * Tests for [MockkDefinition].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class MockDefinitionTests {
+
+ @Test
+ fun createWithDefaults() {
+ val definition = MockkDefinition(typeToMock = EXAMPLE_SERVICE_TYPE)
+ assertThat(definition.name).isNull()
+ assertThat(definition.typeToMock).isEqualTo(EXAMPLE_SERVICE_TYPE)
+ assertThat(definition.extraInterfaces).isEmpty()
+ assertThat(definition.relaxed).isFalse()
+ assertThat(definition.relaxUnitFun).isFalse()
+ assertThat(definition.clear).isEqualTo(MockkClear.AFTER)
+ assertThat(definition.qualifier).isNull()
+ }
+
+ @Test
+ fun createExplicit() {
+ val qualifier = mockk()
+ val definition = MockkDefinition(
+ name = "name",
+ typeToMock = EXAMPLE_SERVICE_TYPE,
+ extraInterfaces = arrayOf(ExampleExtraInterface::class),
+ relaxed = true,
+ relaxUnitFun = true,
+ clear = MockkClear.BEFORE,
+ qualifier = qualifier
+ )
+ assertThat(definition.name).isEqualTo("name")
+ assertThat(definition.typeToMock).isEqualTo(EXAMPLE_SERVICE_TYPE)
+ assertThat(definition.extraInterfaces).containsExactly(ExampleExtraInterface::class)
+ assertThat(definition.relaxed).isTrue()
+ assertThat(definition.relaxUnitFun).isTrue()
+ assertThat(definition.clear).isEqualTo(MockkClear.BEFORE)
+ assertThat(definition.qualifier).isEqualTo(qualifier)
+ }
+
+ @Test
+ fun createMock() {
+ val definition = MockkDefinition(
+ name = "blabla",
+ typeToMock = EXAMPLE_SERVICE_TYPE,
+ extraInterfaces = arrayOf(ExampleExtraInterface::class),
+ relaxed = true,
+ relaxUnitFun = false,
+ clear = MockkClear.BEFORE,
+ qualifier = null
+ )
+ val mock = definition.createMock()
+ assertThat(mock).isInstanceOf(ExampleService::class.java)
+ assertThat(mock).isInstanceOf(ExampleExtraInterface::class.java)
+ assertThat(mock.toString()).contains("blabla")
+
+ // test that it's indeed relaxed
+ assertThatCode { (mock as ExampleService).greeting() }.doesNotThrowAnyException()
+ assertThat(MockkClear.get(mock)).isEqualTo(MockkClear.BEFORE)
+ }
+
+ @Test
+ fun createMockWithRelaxUnitFun() {
+ val definition = MockkDefinition(
+ typeToMock = ResolvableType.forClass(UnitReturningService::class.java),
+ relaxUnitFun = true
+ )
+ val mock = definition.createMock()
+ assertThat(mock).isInstanceOf(UnitReturningService::class.java)
+
+ // test that it's indeed relaxUnitFun
+ assertThatCode { (mock as UnitReturningService).greet() }.doesNotThrowAnyException()
+ // and not relaxed
+ assertThatExceptionOfType(MockKException::class.java).isThrownBy { (mock as UnitReturningService).greetWithResult() }
+ }
+
+ companion object {
+ private val EXAMPLE_SERVICE_TYPE = ResolvableType.forClass(ExampleService::class.java)
+ }
+
+ interface UnitReturningService {
+ fun greet(): Unit
+ fun greetWithResult(): String
+ }
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkPostProcessorTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkPostProcessorTests.kt
new file mode 100644
index 000000000..e83eda012
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkPostProcessorTests.kt
@@ -0,0 +1,319 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.FailingExampleService
+import io.mockk.springmockk.example.RealExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatIllegalStateException
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.FactoryBean
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
+import org.springframework.beans.factory.getBean
+import org.springframework.beans.factory.support.BeanDefinitionRegistry
+import org.springframework.beans.factory.support.RootBeanDefinition
+import org.springframework.boot.test.mock.mockito.MockitoPostProcessor
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Primary
+import org.springframework.core.Ordered
+import org.springframework.test.util.ReflectionTestUtils
+import org.springframework.util.Assert
+
+
+/**
+ * Test for [MockkPostProcessor]. See also the integration tests.
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author Andreas Neiser
+ * @author JB Nizet
+ */
+class MockkPostProcessorTests {
+
+ @Test
+ fun cannotMockMultipleBeans() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ context.register(MultipleBeans::class.java)
+ assertThatIllegalStateException().isThrownBy { context.refresh() }.withMessageContaining(
+ "Unable to register mock bean " + ExampleService::class.java.name
+ + " expected a single matching bean to replace "
+ + "but found [example1, example2]"
+ )
+ }
+
+ @Test
+ fun cannotMockMultipleQualifiedBeans() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ context.register(MultipleQualifiedBeans::class.java)
+ assertThatIllegalStateException().isThrownBy { context.refresh() }
+ .withMessageContaining(
+ ("Unable to register mock bean " + ExampleService::class.java.name
+ + " expected a single matching bean to replace "
+ + "but found [example1, example3]")
+ )
+ }
+
+ @Test
+ fun canMockBeanProducedByFactoryBeanWithObjectTypeAttribute() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ val factoryBeanDefinition = RootBeanDefinition(TestFactoryBean::class.java)
+ factoryBeanDefinition.setAttribute(
+ FactoryBean.OBJECT_TYPE_ATTRIBUTE,
+ SomeInterface::class.java
+ )
+ context.registerBeanDefinition("beanToBeMocked", factoryBeanDefinition)
+ context.register(MockedFactoryBean::class.java)
+ context.refresh()
+ assertThat(context.getBean("beanToBeMocked").isMock).isTrue()
+ }
+
+ @Test
+ fun canMockPrimaryBean() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ context.register(MockPrimaryBean::class.java)
+ context.refresh()
+ assertThat(context.getBean().mock.isMock).isTrue()
+ assertThat(context.getBean().isMock).isTrue()
+ assertThat(context.getBean("examplePrimary").isMock).isTrue()
+ assertThat(context.getBean("exampleQualified").isMock).isFalse()
+ }
+
+ @Test
+ fun canMockQualifiedBeanWithPrimaryBeanPresent() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ context.register(MockQualifiedBean::class.java)
+ context.refresh()
+ assertThat(context.getBean().mock.isMock).isTrue()
+ assertThat(context.getBean().isMock).isFalse()
+ assertThat(context.getBean("examplePrimary").isMock).isFalse()
+ assertThat(context.getBean("exampleQualified").isMock).isTrue()
+ }
+
+ @Test
+ fun canSpyPrimaryBean() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ context.register(SpyPrimaryBean::class.java)
+ context.refresh()
+ assertThat(context.getBean().spy.isMock).isTrue()
+ assertThat(context.getBean().isMock).isTrue()
+ assertThat(context.getBean("examplePrimary").isMock).isTrue()
+ assertThat(context.getBean("exampleQualified").isMock).isFalse()
+ }
+
+ @Test
+ fun canSpyQualifiedBeanWithPrimaryBeanPresent() {
+ val context = AnnotationConfigApplicationContext()
+ MockkPostProcessor.register(context)
+ context.register(SpyQualifiedBean::class.java)
+ context.refresh()
+ assertThat(context.getBean().spy.isMock).isTrue()
+ assertThat(context.getBean().isMock).isFalse()
+ assertThat(context.getBean("examplePrimary").isMock).isFalse()
+ assertThat(context.getBean("exampleQualified").isMock).isTrue()
+ }
+
+ @Test
+ fun postProcessorShouldNotTriggerEarlyInitialization() {
+ val context = AnnotationConfigApplicationContext()
+ context.register(FactoryBeanRegisteringPostProcessor::class.java)
+ MockitoPostProcessor.register(context)
+ context.register(TestBeanFactoryPostProcessor::class.java)
+ context.register(EagerInitBean::class.java)
+ context.refresh()
+ }
+
+ @Configuration
+ @MockkBean(SomeInterface::class)
+ internal class MockedFactoryBean {
+
+ @Bean
+ fun testFactoryBean(): TestFactoryBean {
+ return TestFactoryBean()
+ }
+
+ }
+
+ @Configuration
+ @MockkBean(ExampleService::class)
+ internal class MultipleBeans {
+
+ @Bean
+ fun example1(): ExampleService {
+ return FailingExampleService()
+ }
+
+ @Bean
+ fun example2(): ExampleService {
+ return FailingExampleService()
+ }
+
+ }
+
+ @Configuration
+ internal class MultipleQualifiedBeans {
+
+ @MockkBean
+ @Qualifier("test")
+ lateinit var mock: ExampleService
+
+ @Bean
+ @Qualifier("test")
+ fun example1(): ExampleService {
+ return FailingExampleService()
+ }
+
+ @Bean
+ fun example2(): ExampleService {
+ return FailingExampleService()
+ }
+
+ @Bean
+ @Qualifier("test")
+ fun example3(): ExampleService {
+ return FailingExampleService()
+ }
+
+ }
+
+ @Configuration
+ internal class MockPrimaryBean {
+
+ @MockkBean
+ lateinit var mock: ExampleService
+
+ @Bean
+ @Qualifier("test")
+ fun exampleQualified(): ExampleService {
+ return RealExampleService("qualified")
+ }
+
+ @Bean
+ @Primary
+ fun examplePrimary(): ExampleService {
+ return RealExampleService("primary")
+ }
+
+ }
+
+ @Configuration
+ internal class MockQualifiedBean {
+
+ @MockkBean
+ @Qualifier("test")
+ lateinit var mock: ExampleService
+
+ @Bean
+ @Qualifier("test")
+ fun exampleQualified(): ExampleService {
+ return RealExampleService("qualified")
+ }
+
+ @Bean
+ @Primary
+ fun examplePrimary(): ExampleService {
+ return RealExampleService("primary")
+ }
+
+ }
+
+ @Configuration
+ internal class SpyPrimaryBean {
+
+ @SpykBean
+ lateinit var spy: ExampleService
+
+ @Bean
+ @Qualifier("test")
+ fun exampleQualified(): ExampleService {
+ return RealExampleService("qualified")
+ }
+
+ @Bean
+ @Primary
+ fun examplePrimary(): ExampleService {
+ return RealExampleService("primary")
+ }
+
+ }
+
+ @Configuration
+ internal class SpyQualifiedBean {
+
+ @SpykBean
+ @Qualifier("test")
+ lateinit var spy: ExampleService
+
+ @Bean
+ @Qualifier("test")
+ fun exampleQualified(): ExampleService {
+ return RealExampleService("qualified")
+ }
+
+ @Bean
+ @Primary
+ fun examplePrimary(): ExampleService {
+ return RealExampleService("primary")
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ internal class EagerInitBean {
+
+ @MockkBean
+ lateinit var service: ExampleService
+
+ }
+
+ internal class TestFactoryBean : FactoryBean {
+
+ override fun getObject(): Any {
+ return TestBean()
+ }
+
+ override fun getObjectType(): Class<*>? {
+ return null
+ }
+
+ override fun isSingleton(): Boolean {
+ return true
+ }
+
+ }
+
+ internal class FactoryBeanRegisteringPostProcessor : BeanFactoryPostProcessor, Ordered {
+
+ override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
+ val beanDefinition = RootBeanDefinition(TestFactoryBean::class.java)
+ (beanFactory as BeanDefinitionRegistry).registerBeanDefinition("test", beanDefinition)
+ }
+
+ override fun getOrder(): Int {
+ return Ordered.HIGHEST_PRECEDENCE
+ }
+ }
+
+ internal class TestBeanFactoryPostProcessor : BeanFactoryPostProcessor {
+
+ override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
+ val cache = ReflectionTestUtils.getField(
+ beanFactory,
+ "factoryBeanInstanceCache"
+ ) as Map<*, *>
+ Assert.isTrue(cache.isEmpty(), "Early initialization of factory bean triggered.")
+ }
+ }
+
+ internal interface SomeInterface
+
+ internal class TestBean : SomeInterface
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkTestExecutionListenerTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkTestExecutionListenerTests.kt
new file mode 100644
index 000000000..3c995e131
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/MockkTestExecutionListenerTests.kt
@@ -0,0 +1,113 @@
+package io.mockk.springmockk
+
+import io.mockk.MockKAnnotations
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.getBean
+import org.springframework.context.ApplicationContext
+import org.springframework.test.context.TestContext
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
+import java.io.InputStream
+
+/**
+ * Tests for [MockkTestExecutionListener].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class MockkTestExecutionListenerTests {
+
+ private val listener = MockkTestExecutionListener()
+
+ @MockK
+ private lateinit var applicationContext: ApplicationContext
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var postProcessor: MockkPostProcessor
+
+ @BeforeEach
+ fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+ @Test
+ fun prepareTestInstanceShouldInitMockkAnnotations() {
+ val instance = WithMockkAnnotations()
+ this.listener.prepareTestInstance(mockTestContext(instance))
+ assertThat(instance.mock).isNotNull()
+ }
+
+ @Test
+ fun prepareTestInstanceShouldInjectMockBean() {
+ every { applicationContext.getBean(MockkPostProcessor::class.java) } returns this.postProcessor
+ val instance = WithMockkBean()
+ val testContext = mockTestContext(instance)
+ every { testContext.applicationContext } returns this.applicationContext
+ this.listener.prepareTestInstance(testContext)
+ verify {
+ postProcessor.inject(
+ withArg { assertThat(it.name).isEqualTo("mockBean") },
+ instance,
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun beforeTestMethodShouldDoNothingWhenDirtiesContextAttributeIsNotSet() {
+ val testContext = mockk()
+ every {
+ testContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE)
+ } returns null
+ this.listener.beforeTestMethod(testContext)
+ confirmVerified(postProcessor)
+ }
+
+ @Test
+ fun beforeTestMethodShouldInjectMockBeanWhenDirtiesContextAttributeIsSet() {
+ every { applicationContext.getBean(MockkPostProcessor::class.java) } returns postProcessor
+ val instance = WithMockkBean()
+ val mockTestContext = mockTestContext(instance)
+ every { mockTestContext.applicationContext } returns this.applicationContext
+ every {
+ mockTestContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE)
+ } returns true
+ this.listener.beforeTestMethod(mockTestContext)
+ verify {
+ postProcessor.inject(
+ withArg { assertThat(it.name).isEqualTo("mockBean") },
+ instance,
+ any()
+ )
+ }
+ }
+
+ private fun mockTestContext(instance: Any): TestContext {
+ val testContext = mockk(relaxed = true)
+ every { testContext.testInstance } returns instance
+ every { testContext.testClass } returns instance.javaClass as Class<*>
+ every { testContext.applicationContext } returns this.applicationContext
+ return testContext
+ }
+
+ internal class WithMockkAnnotations {
+
+ @MockK
+ lateinit var mock: InputStream
+
+ }
+
+ internal class WithMockkBean {
+
+ @MockkBean
+ lateinit var mockBean: InputStream
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/QualifierDefinitionTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/QualifierDefinitionTests.kt
new file mode 100644
index 000000000..e98b744f9
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/QualifierDefinitionTests.kt
@@ -0,0 +1,137 @@
+package io.mockk.springmockk
+
+import io.mockk.MockKAnnotations
+import io.mockk.impl.annotations.MockK
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
+import org.springframework.beans.factory.support.RootBeanDefinition
+import org.springframework.context.annotation.Configuration
+import org.springframework.util.ReflectionUtils
+
+
+/**
+ * Tests for [QualifierDefinition].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class QualifierDefinitionTests {
+
+ @MockK(relaxed = true)
+ private lateinit var beanFactory: ConfigurableListableBeanFactory
+
+ @BeforeEach
+ fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+ @Test
+ fun forElementWhenElementIsNotFieldShouldReturnNull() {
+ assertThat(QualifierDefinition.forElement(javaClass)).isNull()
+ }
+
+ @Test
+ fun forElementWhenElementIsFieldWithNoQualifiersShouldReturnNull() {
+ val definition = QualifierDefinition.forElement(ReflectionUtils.findField(ConfigA::class.java, "noQualifier")!!)
+ assertThat(definition).isNull()
+ }
+
+ @Test
+ fun forElementWhenElementIsFieldWithQualifierShouldReturnDefinition() {
+ val definition = QualifierDefinition.forElement(ReflectionUtils.findField(ConfigA::class.java, "directQualifier")!!)
+ assertThat(definition).isNotNull()
+ }
+
+ @Test
+ fun matchesShouldCallBeanFactory() {
+ val field = ReflectionUtils.findField(ConfigA::class.java, "directQualifier")!!
+ val qualifierDefinition = QualifierDefinition.forElement(field)!!
+ qualifierDefinition.matches(this.beanFactory, "bean")
+ verify {
+ beanFactory.isAutowireCandidate(
+ "bean",
+ withArg { assertThat(it.annotatedElement).isEqualTo(field) }
+ )
+ }
+ }
+
+ @Test
+ fun applyToShouldSetQualifierElement() {
+ val field = ReflectionUtils.findField(ConfigA::class.java, "directQualifier")!!
+ val qualifierDefinition = QualifierDefinition.forElement(field)!!
+ val definition = RootBeanDefinition()
+ qualifierDefinition.applyTo(definition)
+ assertThat(definition.qualifiedElement).isEqualTo(field)
+ }
+
+ @Test
+ fun hashCodeAndEqualsShouldWorkOnDifferentClasses() {
+ val directQualifier1 =
+ QualifierDefinition.forElement(ReflectionUtils.findField(ConfigA::class.java, "directQualifier")!!)!!
+ val directQualifier2 =
+ QualifierDefinition.forElement(ReflectionUtils.findField(ConfigB::class.java, "directQualifier")!!)!!
+ val differentDirectQualifier1 =
+ QualifierDefinition.forElement(ReflectionUtils.findField(ConfigA::class.java, "differentDirectQualifier")!!)!!
+ val differentDirectQualifier2 =
+ QualifierDefinition.forElement(ReflectionUtils.findField(ConfigB::class.java, "differentDirectQualifier")!!)!!
+ val customQualifier1 =
+ QualifierDefinition.forElement(ReflectionUtils.findField(ConfigA::class.java, "customQualifier")!!)!!
+ val customQualifier2 =
+ QualifierDefinition.forElement(ReflectionUtils.findField(ConfigB::class.java, "customQualifier")!!)!!
+
+ assertThat(directQualifier1.hashCode()).isEqualTo(directQualifier2.hashCode())
+ assertThat(differentDirectQualifier1.hashCode())
+ .isEqualTo(differentDirectQualifier2.hashCode())
+ assertThat(customQualifier1.hashCode()).isEqualTo(customQualifier2.hashCode())
+ assertThat(differentDirectQualifier1).isEqualTo(differentDirectQualifier1)
+ .isEqualTo(differentDirectQualifier2).isNotEqualTo(directQualifier2)
+ assertThat(directQualifier1).isEqualTo(directQualifier1)
+ .isEqualTo(directQualifier2).isNotEqualTo(differentDirectQualifier1)
+ assertThat(customQualifier1).isEqualTo(customQualifier1)
+ .isEqualTo(customQualifier2).isNotEqualTo(differentDirectQualifier1)
+ }
+
+ @Configuration
+ internal class ConfigA {
+
+ @MockkBean
+ private lateinit var noQualifier: Any
+
+ @MockkBean
+ @Qualifier("test")
+ private lateinit var directQualifier: Any
+
+ @MockkBean
+ @Qualifier("different")
+ private lateinit var differentDirectQualifier: Any
+
+ @MockkBean
+ @CustomQualifier
+ private lateinit var customQualifier: Any
+
+ }
+
+ internal class ConfigB {
+
+ @MockkBean
+ @Qualifier("test")
+ private lateinit var directQualifier: Any
+
+ @MockkBean
+ @Qualifier("different")
+ private lateinit var differentDirectQualifier: Any
+
+ @MockkBean
+ @CustomQualifier
+ private lateinit var customQualifier: Any
+
+ }
+
+ @Qualifier
+ @Target(AnnotationTarget.FIELD, AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER)
+ annotation class CustomQualifier
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..5924b3f54
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.kt
@@ -0,0 +1,38 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a configuration class can be used to spy existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnConfigurationClassForExistingBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+ @Configuration
+ @SpykBean(SimpleExampleService::class)
+ @Import(ExampleServiceCaller::class, SimpleExampleService::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..3b72e4009
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.kt
@@ -0,0 +1,38 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a configuration class can be used to inject new spy instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnConfigurationClassForNewBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+ @Configuration
+ @SpykBean(SimpleExampleService::class)
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..6b38017c5
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.kt
@@ -0,0 +1,47 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a field on a `@Configuration` class can be used to
+ * replace existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var config: Config
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { config.exampleService.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class, SimpleExampleService::class)
+ internal class Config {
+
+ @SpykBean
+ lateinit var exampleService: ExampleService
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..1e2a44380
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.kt
@@ -0,0 +1,46 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a field on a `@Configuration` class can be used to inject
+ * new spy instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnConfigurationFieldForNewBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var config: Config
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { config.exampleService.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config {
+
+ @SpykBean
+ lateinit var exampleService: SimpleExampleService
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnContextHierarchyIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnContextHierarchyIntegrationTests.kt
new file mode 100644
index 000000000..2ba834bd3
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnContextHierarchyIntegrationTests.kt
@@ -0,0 +1,66 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.MockBeanOnContextHierarchyIntegrationTests.ParentConfig
+import io.mockk.springmockk.SpyBeanOnContextHierarchyIntegrationTests.ChildConfig
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.getBean
+import org.springframework.beans.factory.getBeanNamesForType
+import org.springframework.context.ApplicationContext
+import org.springframework.context.ApplicationContextAware
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.ContextHierarchy
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] can be used with a [ContextHierarchy].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@ContextHierarchy(
+ ContextConfiguration(classes = [ParentConfig::class]),
+ ContextConfiguration(classes = [ChildConfig::class])
+)
+class SpyBeanOnContextHierarchyIntegrationTests {
+
+ @Autowired
+ private lateinit var childConfig: ChildConfig
+
+ @Test
+ fun testSpying() {
+ val context = this.childConfig.context
+ val parentContext = context.parent!!
+ assertThat(parentContext.getBeanNamesForType()).hasSize(1)
+ assertThat(parentContext.getBeanNamesForType()).hasSize(0)
+ assertThat(context.getBeanNamesForType()).hasSize(0)
+ assertThat(context.getBeanNamesForType()).hasSize(1)
+ assertThat(context.getBean()).isNotNull()
+ assertThat(context.getBean()).isNotNull()
+ }
+
+ @Configuration
+ @SpykBean(SimpleExampleService::class)
+ internal class ParentConfig
+
+ @Configuration
+ @SpykBean(ExampleServiceCaller::class)
+ internal class ChildConfig : ApplicationContextAware {
+
+ lateinit var context: ApplicationContext
+
+ override fun setApplicationContext(applicationContext: ApplicationContext) {
+ this.context = applicationContext
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestClassForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestClassForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..560ead607
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestClassForExistingBeanIntegrationTests.kt
@@ -0,0 +1,38 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class can be used to replace existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@SpykBean(SimpleExampleService::class)
+class SpyBeanOnTestClassForExistingBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class, SimpleExampleService::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestClassForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestClassForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..4d5bf5730
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestClassForNewBeanIntegrationTests.kt
@@ -0,0 +1,38 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class can be used to inject new spy instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@SpykBean(SimpleExampleService::class)
+class SpyBeanOnTestClassForNewBeanIntegrationTests {
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.kt
new file mode 100644
index 000000000..d0b3f397b
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.kt
@@ -0,0 +1,40 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class field can be used to replace existing beans when
+ * the context is cached. This test is identical to
+ * [SpyBeanOnTestFieldForExistingBeanIntegrationTests] so one of them should trigger
+ * application context caching.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see SpyBeanOnTestFieldForExistingBeanIntegrationTests
+ */
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [SpyBeanOnTestFieldForExistingBeanConfig::class])
+class SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests {
+
+ @SpykBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanConfig.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanConfig.kt
new file mode 100644
index 000000000..442df56b3
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanConfig.kt
@@ -0,0 +1,19 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+
+
+/**
+ * Config for [SpyBeanOnTestFieldForExistingBeanIntegrationTests] and
+ * [SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests]. Extracted to a shared
+ * config to trigger caching.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@Configuration
+@Import(ExampleServiceCaller::class, SimpleExampleService::class)
+class SpyBeanOnTestFieldForExistingBeanConfig
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanIntegrationTests.kt
new file mode 100644
index 000000000..9ad129b0d
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanIntegrationTests.kt
@@ -0,0 +1,37 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class field can be used to replace existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests
+ */
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [SpyBeanOnTestFieldForExistingBeanConfig::class])
+class SpyBeanOnTestFieldForExistingBeanIntegrationTests {
+
+ @SpykBean
+ private lateinit var exampleService: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.kt
new file mode 100644
index 000000000..b9f65fa82
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.kt
@@ -0,0 +1,72 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.*
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class field can be used to replace existing bean while
+ * preserving qualifiers.
+ *
+ * @author Andreas Neiser
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests {
+
+ @SpykBean
+ @CustomQualifier
+ private lateinit var service: ExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Autowired
+ private lateinit var applicationContext: ApplicationContext
+
+ @Test
+ @Throws(Exception::class)
+ fun testMocking() {
+ this.caller.sayGreeting()
+ verify { service.greeting() }
+ }
+
+ @Test
+ fun onlyQualifiedBeanIsReplaced() {
+ assertThat(this.applicationContext.getBean("service")).isSameAs(this.service)
+ val anotherService = this.applicationContext.getBean(
+ "anotherService",
+ ExampleService::class.java
+ )
+ assertThat(anotherService.greeting()).isEqualTo("Another")
+ }
+
+ @Configuration
+ internal class TestConfig {
+
+ @Bean
+ fun service(): CustomQualifierExampleService {
+ return CustomQualifierExampleService()
+ }
+
+ @Bean
+ fun anotherService(): ExampleService {
+ return RealExampleService("Another")
+ }
+
+ @Bean
+ fun controller(@CustomQualifier service: ExampleService): ExampleServiceCaller {
+ return ExampleServiceCaller(service)
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.kt
new file mode 100644
index 000000000..865303902
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.kt
@@ -0,0 +1,54 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.SpyBeanOnTestFieldForExistingCircularBeansConfig
+import io.mockk.verify
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [@SpykBean][SpykBean] on a test class field can be used to replace existing
+ * beans with circular dependencies.
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@ContextConfiguration(classes = [SpyBeanOnTestFieldForExistingCircularBeansConfig::class])
+internal class SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests {
+ @SpykBean
+ private lateinit var one: One
+
+ @Autowired
+ private lateinit var two: Two
+
+ @Test
+ fun beanWithCircularDependenciesCanBeSpied() {
+ two.callOne()
+
+ verify { one.someMethod() }
+ }
+
+ @Import(One::class, Two::class)
+ internal class SpyBeanOnTestFieldForExistingCircularBeansConfig
+
+ internal class One {
+ @Autowired
+ private lateinit var two: Two
+
+ fun someMethod() {}
+ }
+
+ internal class Two {
+ @Autowired
+ private lateinit var one: One
+
+ fun callOne() {
+ one.someMethod()
+ }
+ }
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.kt
new file mode 100644
index 000000000..c6b6578b2
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.kt
@@ -0,0 +1,54 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleGenericService
+import io.mockk.springmockk.example.ExampleGenericServiceCaller
+import io.mockk.springmockk.example.SimpleExampleIntegerGenericService
+import io.mockk.springmockk.example.SimpleExampleStringGenericService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class field can be used to replace existing beans.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests {
+
+ // gh-7625
+
+ @SpykBean
+ private lateinit var exampleService: ExampleGenericService
+
+ @Autowired
+ private lateinit var caller: ExampleGenericServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say 123 simple")
+ verify { exampleService.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleGenericServiceCaller::class, SimpleExampleIntegerGenericService::class)
+ internal class SpyBeanOnTestFieldForExistingBeanConfig {
+
+ @Bean
+ fun simpleExampleStringGenericService(): ExampleGenericService {
+ // In order to trigger issue we need a method signature that returns the
+ // generic type not the actual implementation class
+ return SimpleExampleStringGenericService()
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.kt
new file mode 100644
index 000000000..48fd4791c
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.kt
@@ -0,0 +1,58 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleGenericStringServiceCaller
+import io.mockk.springmockk.example.SimpleExampleStringGenericService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.context.annotation.Primary
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class field can be used to inject a spy instance when
+ * there are multiple candidates and one is primary.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests {
+
+ @SpykBean
+ private lateinit var spy: SimpleExampleStringGenericService
+
+ @Autowired
+ private lateinit var caller: ExampleGenericStringServiceCaller
+
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say two")
+ assertThat(this.spy.toString()).contains("two")
+ verify { spy.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleGenericStringServiceCaller::class)
+ internal class Config {
+
+ @Bean
+ fun one(): SimpleExampleStringGenericService {
+ return SimpleExampleStringGenericService("one")
+ }
+
+ @Bean
+ @Primary
+ fun two(): SimpleExampleStringGenericService {
+ return SimpleExampleStringGenericService("two")
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForNewBeanIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForNewBeanIntegrationTests.kt
new file mode 100644
index 000000000..ec3217837
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanOnTestFieldForNewBeanIntegrationTests.kt
@@ -0,0 +1,39 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+/**
+ * Test [SpykBean] on a test class field can be used to inject new spy instances.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanOnTestFieldForNewBeanIntegrationTests {
+
+ @SpykBean
+ private lateinit var exampleService: SimpleExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ fun testSpying() {
+ assertThat(this.caller.sayGreeting()).isEqualTo("I say simple")
+ verify { caller.service.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.kt
new file mode 100644
index 000000000..ad7b6367f
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.kt
@@ -0,0 +1,70 @@
+package io.mockk.springmockk
+
+import io.mockk.MockKException
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.cache.CacheManager
+import org.springframework.cache.annotation.Cacheable
+import org.springframework.cache.annotation.EnableCaching
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager
+import org.springframework.cache.interceptor.CacheResolver
+import org.springframework.cache.interceptor.SimpleCacheResolver
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.stereotype.Service
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] when mixed with Spring AOP.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see [5837](https://github.com/spring-projects/spring-boot/issues/5837)
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanWithAopProxyAndNotProxyTargetAwareTests {
+
+ @SpykBean
+ private lateinit var dateService: DateService
+
+ @Test
+ fun verifyShouldUseProxyTarget() {
+ this.dateService.getDate(false)
+ assertThatExceptionOfType(MockKException::class.java).isThrownBy {
+ verify(exactly = 1) { dateService.getDate(false) }
+ }
+ }
+
+ @Configuration
+ @EnableCaching(proxyTargetClass = true)
+ @Import(DateService::class)
+ internal class Config {
+
+ @Bean
+ fun cacheResolver(cacheManager: CacheManager): CacheResolver {
+ val resolver = SimpleCacheResolver()
+ resolver.cacheManager = cacheManager
+ return resolver
+ }
+
+ @Bean
+ fun cacheManager(): ConcurrentMapCacheManager {
+ val cacheManager = ConcurrentMapCacheManager()
+ cacheManager.setCacheNames(listOf("test"))
+ return cacheManager
+ }
+ }
+
+ @Service
+ internal class DateService {
+
+ @Cacheable(cacheNames = ["test"])
+ fun getDate(arg: Boolean) = System.nanoTime()
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithAopProxyTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithAopProxyTests.kt
new file mode 100644
index 000000000..ac9d4fd61
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithAopProxyTests.kt
@@ -0,0 +1,86 @@
+package io.mockk.springmockk
+
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.cache.CacheManager
+import org.springframework.cache.annotation.Cacheable
+import org.springframework.cache.annotation.EnableCaching
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager
+import org.springframework.cache.interceptor.CacheResolver
+import org.springframework.cache.interceptor.SimpleCacheResolver
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.stereotype.Service
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] when mixed with Spring AOP.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ * @see [5837](https://github.com/spring-projects/spring-boot/issues/5837)
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanWithAopProxyTests {
+
+ @SpykBean
+ private lateinit var dateService: DateService
+
+ /**
+ * This test currently fails, because the issue [5837](https://github.com/spring-projects/spring-boot/issues/5837)
+ * also exists for MockK. Unfortunately, I have no clear idea of how to fix it.
+ */
+ @Test
+ @Disabled(
+ """
+ This test currently fails, because the issue [5837](https://github.com/spring-projects/spring-boot/issues/5837)
+ also exists for MockK. Unfortunately, I have no clear idea of how to fix it.
+ """
+ )
+ fun verifyShouldUseProxyTarget() {
+ val d1 = this.dateService.getDate(false)
+ Thread.sleep(200)
+ val d2 = this.dateService.getDate(false)
+ assertThat(d1).isEqualTo(d2)
+ verify(exactly = 2) { dateService.getDate(false) }
+ verify(exactly = 2) { dateService.getDate(eq(false)) }
+ verify(exactly = 2) { dateService.getDate(any()) }
+ }
+
+ @Configuration
+ @EnableCaching(proxyTargetClass = true)
+ @Import(DateService::class)
+ internal class Config {
+
+ @Bean
+ fun cacheResolver(cacheManager: CacheManager): CacheResolver {
+ val resolver = SimpleCacheResolver()
+ resolver.cacheManager = cacheManager
+ return resolver
+ }
+
+ @Bean
+ fun cacheManager(): ConcurrentMapCacheManager {
+ val cacheManager = ConcurrentMapCacheManager()
+ cacheManager.setCacheNames(listOf("test"))
+ return cacheManager
+ }
+
+ }
+
+ @Service
+ internal class DateService {
+
+ @Cacheable(cacheNames = ["test"])
+ fun getDate(arg: Boolean): Long? {
+ return System.nanoTime()
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.kt
new file mode 100644
index 000000000..2c79b7d33
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.kt
@@ -0,0 +1,44 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.SimpleExampleService
+import io.mockk.verify
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.annotation.DirtiesContext
+import org.springframework.test.annotation.DirtiesContext.ClassMode
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Integration tests for using [SpykBean] with [DirtiesContext] and
+ * [ClassMode.BEFORE_EACH_TEST_METHOD].
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
+class SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests {
+
+ @SpykBean
+ private lateinit var exampleService: SimpleExampleService
+
+ @Autowired
+ private lateinit var caller: ExampleServiceCaller
+
+ @Test
+ @Throws(Exception::class)
+ fun testSpying() {
+ this.caller.sayGreeting()
+ verify { exampleService.greeting() }
+ }
+
+ @Configuration
+ @Import(ExampleServiceCaller::class)
+ internal class Config
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithJdkProxyTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithJdkProxyTests.kt
new file mode 100644
index 000000000..e1b597c7c
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithJdkProxyTests.kt
@@ -0,0 +1,60 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import java.lang.reflect.Proxy
+
+
+/**
+ * Tests for [@SpykBean][SpykBean] with a JDK proxy.
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanWithJdkProxyTests {
+ @Autowired
+ private lateinit var service: ExampleService
+
+ @SpykBean
+ private lateinit var repository: ExampleRepository
+
+ @Test
+ fun jdkProxyCanBeSpied() {
+ val example = service.find("id")
+ assertThat(example.id).isEqualTo("id")
+ verify { repository.find("id") }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @Import(ExampleService::class)
+ class Config {
+ @Bean
+ fun dateService(): ExampleRepository {
+ return Proxy.newProxyInstance(
+ javaClass.classLoader,
+ arrayOf(ExampleRepository::class.java)
+ ) { _, _, args -> Example(args[0] as String) } as ExampleRepository
+ }
+ }
+
+ class ExampleService(private val repository: ExampleRepository) {
+ fun find(id: String): Example {
+ return repository.find(id)
+ }
+ }
+
+ interface ExampleRepository {
+ fun find(id: String): Example
+ }
+
+ class Example(val id: String)
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithNameOnTestFieldForMultipleExistingBeansTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithNameOnTestFieldForMultipleExistingBeansTests.kt
new file mode 100644
index 000000000..bc0268051
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpyBeanWithNameOnTestFieldForMultipleExistingBeansTests.kt
@@ -0,0 +1,47 @@
+package io.mockk.springmockk
+
+import io.mockk.springmockk.example.SimpleExampleStringGenericService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+
+/**
+ * Test [SpykBean] on a test class field can be used to inject a spy instance when
+ * there are multiple candidates and one is chosen using the name attribute.
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class SpyBeanWithNameOnTestFieldForMultipleExistingBeansTests {
+
+ @SpykBean(name = "two")
+ private lateinit var spy: SimpleExampleStringGenericService
+
+ @Test
+ fun testSpying() {
+ assertThat(spy.isMock).isTrue()
+ assertThat(spy.toString()).contains("two")
+ }
+
+ @Configuration
+ internal class Config {
+
+ @Bean
+ fun one(): SimpleExampleStringGenericService {
+ return SimpleExampleStringGenericService("one")
+ }
+
+ @Bean
+ fun two(): SimpleExampleStringGenericService {
+ return SimpleExampleStringGenericService("two")
+ }
+
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpykDefinitionTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpykDefinitionTests.kt
new file mode 100644
index 000000000..e1948e145
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/SpykDefinitionTests.kt
@@ -0,0 +1,87 @@
+package io.mockk.springmockk
+
+import io.mockk.mockk
+import io.mockk.springmockk.example.ExampleService
+import io.mockk.springmockk.example.ExampleServiceCaller
+import io.mockk.springmockk.example.RealExampleService
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
+import org.junit.jupiter.api.Test
+import org.springframework.core.ResolvableType
+
+
+/**
+ * Tests for [SpykDefinition].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class SpykDefinitionTests {
+
+ @Test
+ fun createWithDefaults() {
+ val definition = SpykDefinition(typeToSpy = REAL_SERVICE_TYPE)
+ assertThat(definition.name).isNull()
+ assertThat(definition.typeToSpy).isEqualTo(REAL_SERVICE_TYPE)
+ assertThat(definition.clear).isEqualTo(MockkClear.AFTER)
+ assertThat(definition.qualifier).isNull()
+ }
+
+ @Test
+ fun createExplicit() {
+ val qualifier = mockk()
+ val definition = SpykDefinition(
+ name = "name",
+ typeToSpy = REAL_SERVICE_TYPE,
+ clear = MockkClear.BEFORE,
+ qualifier = qualifier
+ )
+ assertThat(definition.name).isEqualTo("name")
+ assertThat(definition.typeToSpy).isEqualTo(REAL_SERVICE_TYPE)
+ assertThat(definition.clear).isEqualTo(MockkClear.BEFORE)
+ assertThat(definition.qualifier).isEqualTo(qualifier)
+ }
+
+ @Test
+ fun createSpy() {
+ val definition = SpykDefinition(
+ name = "name",
+ typeToSpy = REAL_SERVICE_TYPE,
+ clear = MockkClear.BEFORE
+ )
+ val spy = definition.createSpy(RealExampleService("hello"))
+ assertThat(spy).isInstanceOf(ExampleService::class.java)
+ assertThat(spy.toString()).contains("name")
+ assertThat(MockkClear.get(spy)).isEqualTo(MockkClear.BEFORE)
+ }
+
+ @Test
+ fun createSpyWhenWrongInstanceShouldThrowException() {
+ val definition = SpykDefinition(
+ name = "name",
+ typeToSpy = REAL_SERVICE_TYPE,
+ clear = MockkClear.BEFORE
+ )
+ assertThatIllegalArgumentException()
+ .isThrownBy { definition.createSpy(ExampleServiceCaller(RealExampleService("hello"))) }
+ .withMessageContaining("must be an instance of")
+ }
+
+ @Test
+ fun createSpyTwice() {
+ val definition = SpykDefinition(
+ name = "name",
+ typeToSpy = REAL_SERVICE_TYPE,
+ clear = MockkClear.BEFORE
+ )
+ var instance: Any = RealExampleService("hello")
+ instance = definition.createSpy(instance)
+ definition.createSpy(instance)
+ }
+
+ companion object {
+
+ private val REAL_SERVICE_TYPE = ResolvableType.forClass(RealExampleService::class.java)
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/VerifyAllTests.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/VerifyAllTests.kt
new file mode 100644
index 000000000..b1468d6f9
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/VerifyAllTests.kt
@@ -0,0 +1,30 @@
+package io.mockk.springmockk
+
+import io.mockk.every
+import io.mockk.verifyAll
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Component
+import org.springframework.test.context.junit.jupiter.SpringExtension
+
+/**
+ * Tests for issue https://github.com/Ninja-Squad/springmockk/issues/90
+ * @author JB Nizet
+ */
+@ExtendWith(SpringExtension::class)
+class VerifyAllTests @Autowired constructor(@MockkBean val testService: VerifyAllTestService) {
+
+ @Test
+ fun testService() {
+ every { testService.one() } returns 2
+ assertThat(testService.one()).isEqualTo(2)
+ verifyAll { testService.one() }
+ }
+}
+
+@Component
+class VerifyAllTestService {
+ fun one() = 1
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/CustomQualifier.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/CustomQualifier.kt
new file mode 100644
index 000000000..601a1592c
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/CustomQualifier.kt
@@ -0,0 +1,13 @@
+package io.mockk.springmockk.example
+
+import org.springframework.beans.factory.annotation.Qualifier
+
+/**
+ * Custom qualifier for testing.
+ *
+ * @author Stephane Nicoll
+ * @author JB Nizet
+ */
+@Qualifier
+@Target(AnnotationTarget.FIELD, AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER)
+annotation class CustomQualifier
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/CustomQualifierExampleService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/CustomQualifierExampleService.kt
new file mode 100644
index 000000000..7e412b0bf
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/CustomQualifierExampleService.kt
@@ -0,0 +1,16 @@
+package io.mockk.springmockk.example
+
+/**
+ * An [ExampleService] that uses a custom qualifier.
+ *
+ * @author Andy Wilkinson
+ * @author JB Nizet
+ */
+@CustomQualifier
+class CustomQualifierExampleService : ExampleService {
+
+ override fun greeting(): String {
+ return "CustomQualifier"
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleExtraInterface.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleExtraInterface.kt
new file mode 100644
index 000000000..91418b2ac
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleExtraInterface.kt
@@ -0,0 +1,13 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example extra interface for mocking tests.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+interface ExampleExtraInterface {
+
+ fun doExtra(): String
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericService.kt
new file mode 100644
index 000000000..d91235bf7
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericService.kt
@@ -0,0 +1,14 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example service interface for mocking tests.
+ *
+ * @param T the generic type
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+interface ExampleGenericService {
+
+ fun greeting(): T
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericServiceCaller.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericServiceCaller.kt
new file mode 100644
index 000000000..5e080fbdd
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericServiceCaller.kt
@@ -0,0 +1,19 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example bean for mocking tests that calls [ExampleGenericService].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class ExampleGenericServiceCaller(
+ val integerService: ExampleGenericService,
+ val stringService: ExampleGenericService
+) {
+
+ fun sayGreeting(): String {
+ return ("I say " + this.integerService.greeting() + " "
+ + this.stringService.greeting())
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericStringServiceCaller.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericStringServiceCaller.kt
new file mode 100644
index 000000000..5b9443a87
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleGenericStringServiceCaller.kt
@@ -0,0 +1,13 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example bean for mocking tests that calls [ExampleGenericService].
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class ExampleGenericStringServiceCaller(private val stringService: ExampleGenericService) {
+
+ fun sayGreeting() = "I say " + this.stringService.greeting()
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleService.kt
new file mode 100644
index 000000000..e80a55e23
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleService.kt
@@ -0,0 +1,12 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example service interface for mocking tests.
+ *
+ * @author Phillip Webb
+ */
+interface ExampleService {
+
+ fun greeting(): String
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleServiceCaller.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleServiceCaller.kt
new file mode 100644
index 000000000..91509ae71
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/ExampleServiceCaller.kt
@@ -0,0 +1,14 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example bean for mocking tests that calls [ExampleService].
+ *
+ * @author Phillip Webb
+ */
+class ExampleServiceCaller(val service: ExampleService) {
+
+ fun sayGreeting(): String {
+ return "I say " + this.service.greeting()
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/FailingExampleService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/FailingExampleService.kt
new file mode 100644
index 000000000..269cdfdeb
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/FailingExampleService.kt
@@ -0,0 +1,17 @@
+package io.mockk.springmockk.example
+
+import org.springframework.stereotype.Service
+
+/**
+ * An [ExampleService] that always throws an exception.
+ *
+ * @author Phillip Webb
+ */
+@Service
+class FailingExampleService : ExampleService {
+
+ override fun greeting(): String {
+ throw IllegalStateException("Failed")
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/RealExampleService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/RealExampleService.kt
new file mode 100644
index 000000000..06ef5b6f3
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/RealExampleService.kt
@@ -0,0 +1,15 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example service implementation for spy tests.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+open class RealExampleService(private val greeting: String) : ExampleService {
+
+ override fun greeting(): String {
+ return this.greeting
+ }
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleIntegerGenericService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleIntegerGenericService.kt
new file mode 100644
index 000000000..4879f03a5
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleIntegerGenericService.kt
@@ -0,0 +1,13 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example generic service implementation for spy tests.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class SimpleExampleIntegerGenericService : ExampleGenericService {
+
+ override fun greeting() = 123
+
+}
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleService.kt
new file mode 100644
index 000000000..bdc5748bf
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleService.kt
@@ -0,0 +1,10 @@
+package io.mockk.springmockk.example
+
+
+/**
+ * Example service implementation for spy tests.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class SimpleExampleService : RealExampleService("simple")
diff --git a/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleStringGenericService.kt b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleStringGenericService.kt
new file mode 100644
index 000000000..8d564b96c
--- /dev/null
+++ b/modules/springmockk/src/test/kotlin/io/mockk/springmockk/example/SimpleExampleStringGenericService.kt
@@ -0,0 +1,13 @@
+package io.mockk.springmockk.example
+
+/**
+ * Example generic service implementation for spy tests.
+ *
+ * @author Phillip Webb
+ * @author JB Nizet
+ */
+class SimpleExampleStringGenericService(private val greeting: String = "simple") : ExampleGenericService {
+
+ override fun greeting() = this.greeting
+
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6b01994f3..7c141e10b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,6 +28,7 @@ include(
":modules:mockk-agent",
":modules:mockk-core",
":modules:mockk-dsl",
+ ":modules:springmockk",
":test-modules:client-tests",
":test-modules:performance-tests",