diff --git a/MAINTAINING.md b/MAINTAINING.md deleted file mode 100644 index 5aa6e10cc..000000000 --- a/MAINTAINING.md +++ /dev/null @@ -1,29 +0,0 @@ - -## Releasing mockk - - Main thing before release is to test it well. There was cases when after upgrade of library Android Instrumented was not tested and half a year was not usable. - - So checklist: - - - [ ] make sure all required PRs are merged - - [ ] run test-suite in Gradle - - [ ] run test-suite from Android Studio or IntelliJ from emulator (Android Instrumented tests) - - [ ] release to local maven repo by running `gradle publish` with the `localrepo` repository uncommented in `mockk-publishing.gradle.kts` - - [ ] do quick testing with this local maven repo: basics, if documentation is loading, if all dependencies are fine - - [ ] change version to RELEASE version (i.e. remove -SNAPSHOT from version) - - [ ] bump if needed major or minor (resetting everything afterwards) - - [ ] commit it - - [ ] tag it - - [ ] redirect release process to oss.sonatype by uncommenting the sonatype repository in `mockk-publishing.gradle.kts` - - [ ] release from Gradle with `gradle publish -Dorg.gradle.parallel=false` (apparently, sonatype does not like parallel builds) - - [ ] goto oss.sonatype - - [ ] find io.mockk repo - - [ ] check state of dependencies (are sizes okay), maybe download one - - [ ] close repo - - [ ] release repo - - [ ] wait till it appears on Maven central - - [ ] bump version and append -SNAPSHOT - - [ ] commit - - [ ] push to GitHub - - [ ] create GitHub release based on tag and describe changes there - - [ ] announce on reddit/kotlinlang/potentially at "announcement place" on mockk.io diff --git a/MATRIX.md b/MATRIX.md deleted file mode 100644 index 7ec97f9b6..000000000 --- a/MATRIX.md +++ /dev/null @@ -1,323 +0,0 @@ - - -### Results of matrix tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureJDKAndroid
6891011UnitAIT ≥P
Additional answers 0.09 0.06 0.10 0.05 0.07 0.08
Annotations 0.16 0.15 0.15 0.17 0.20 0.15
Answers 0.15 0.09 0.08 0.08 0.09 0.08
Arrays 0.35 0.15 0.13 0.16 0.17 0.15
Backing field 0.03 0.03 0.04 0.03 0.08 0.03
Chainied calls 0.34 0.19 0.15 0.13 0.18 0.17
Clearing mocks 0.07 0.03 0.03 0.03 0.03 0.03
Coroutines 0.11 0.08 0.08 0.07 0.07 0.08
Enums 0.17 0.25 0.27 0.30 0.22 0.23
Extension functions 0.11 0.24 0.25 0.26 0.22 0.21
Initialization block 0.05 0.09 0.09 0.09 0.13 0.09
Matchers 0.49 0.27 0.27 0.25 0.27 0.26
Nulls 0.09 0.11 0.14 0.15 0.12 0.11
Object mocks 0.09 0.07 0.08 0.10 0.08 0.10
Partial argument matching 1.18 0.59 0.61 0.33 0.46 0.58
Private functions 0.20 0.32 0.36 0.36 0.36 0.32
Spies 0.11 0.14 0.14 0.17 0.14 0.14
Varargs 0.08 0.03 0.04 0.03 0.03 0.04
Verification errors 0.23 0.12 0.13 0.14 0.13 0.11
Verification counts 0.41 0.22 0.20 0.20 0.17 0.19
Verify test 0.18 0.13 0.09 0.14 0.14 0.12
Many answers 0.07 0.12 0.09 0.09 0.09 0.09
Constructor mocking 0.25 0.29 0.28 0.32 0.27 0.30
#25 0.04 0.03 0.04 0.04 0.05 0.03
#31 0.02 0.02 0.02 0.02 0.02 0.01
#35 1.59 1.55 1.66 1.82 1.56 1.58
#36 5.95 3.15 3.65 4.02 8.18 3.56
#47 0.14 0.15 0.15 0.18 0.15 0.14
#48 0.20 0.16 0.16 0.15 0.31 0.14
#51 0.19 0.19 0.22 0.23 0.31 0.20
#70 0.11 0.10 0.09 0.10 0.20 0.13
(other tests) 6.88 8.70 8.33 8.41 7.90 9.24
All tests 20.12 17.82 18.10 18.62 22.39 18.70
- diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c4cb6958c..5fd40a3ab 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinPluginVersion") implementation("org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin:$kotlinxKover") + implementation("org.jetbrains.kotlin.plugin.spring:org.jetbrains.kotlin.plugin.spring.gradle.plugin:$kotlinPluginVersion") implementation("com.android.tools.build:gradle:$androidGradle") implementation("org.jetbrains.dokka:dokka-gradle-plugin:$dokka") diff --git a/buildSrc/src/main/kotlin/buildsrc/config/Deps.kt b/buildSrc/src/main/kotlin/buildsrc/config/Deps.kt index 700102cc2..f2d75780e 100644 --- a/buildSrc/src/main/kotlin/buildsrc/config/Deps.kt +++ b/buildSrc/src/main/kotlin/buildsrc/config/Deps.kt @@ -16,8 +16,14 @@ object Deps { const val coroutines = "1.6.4" const val slfj = "2.0.5" const val logback = "1.4.5" - const val junitJupiter = "5.8.2" + const val junitJupiter = "5.10.2" const val junit4 = "4.13.2" + const val assertj = "3.25.3" + + const val kotlinReflect = "1.7.21" + + const val springBoot = "3.2.2" + const val spring = "6.1.3" const val byteBuddy = "1.14.6" const val objenesis = "3.3" @@ -40,6 +46,14 @@ object Deps { const val junit4 = "junit:junit:${Versions.junit4}" const val junitJupiter = "org.junit.jupiter:junit-jupiter:${Versions.junitJupiter}" + const val junitJupiterParams = "org.junit.jupiter:junit-jupiter-params:${Versions.junitJupiter}" + const val assertj = "org.assertj:assertj-core:${Versions.assertj}" + + const val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinReflect}" + + const val springBootTest = "org.springframework.boot:spring-boot-test:${Versions.springBoot}" + const val springTest = "org.springframework:spring-test:${Versions.spring}" + const val springContext = "org.springframework:spring-context:${Versions.spring}" const val kotlinCoroutinesBom = "org.jetbrains.kotlinx:kotlinx-coroutines-bom:${Versions.coroutines}" const val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core" diff --git a/buildSrc/src/main/kotlin/buildsrc/config/publishing.kt b/buildSrc/src/main/kotlin/buildsrc/config/publishing.kt index 7399cea92..6235255bf 100644 --- a/buildSrc/src/main/kotlin/buildsrc/config/publishing.kt +++ b/buildSrc/src/main/kotlin/buildsrc/config/publishing.kt @@ -30,6 +30,12 @@ fun MavenPublication.createMockKPom( name.set("Mattia Tommasone") email.set("raibaz@gmail.com") } + developer { + // springmockk author + id.set("jnizet") + name.set("Jean-Baptiste Nizet") + email.set("jb@ninja-squad.com") + } } licenses { diff --git a/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm-spring.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm-spring.gradle.kts new file mode 100644 index 000000000..a5f0d0129 --- /dev/null +++ b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm-spring.gradle.kts @@ -0,0 +1,23 @@ +package buildsrc.convention + +import buildsrc.config.Deps +import org.gradle.api.JavaVersion +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("buildsrc.convention.kotlin-jvm") + id("org.jetbrains.kotlin.plugin.spring") +} + +tasks.withType().configureEach { + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } +} diff --git a/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts index 59c435593..8e84b6575 100644 --- a/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts @@ -14,7 +14,7 @@ plugins { } java { - withJavadocJar() +// withJavadocJar() withSourcesJar() } @@ -31,6 +31,7 @@ tasks.withType().configureEach { } } -tasks.named("javadocJar") { +val javadocJar by tasks.registering(Jar::class) { from(tasks.dokkaJavadoc) -} + archiveClassifier.set("javadoc") +} \ No newline at end of file diff --git a/modules/springmockk/README.md b/modules/springmockk/README.md new file mode 100644 index 000000000..4f31bf675 --- /dev/null +++ b/modules/springmockk/README.md @@ -0,0 +1,135 @@ +# Inclusion of springmockk into mockk + +It was agreed that further maintenance of springmockk is done by mockk community +and project springmockk codebase is included in mockk. + +Codebase is based on version springmockk 4.0.2 dcbe643 and tuned by +kkurczewski in https://github.com/kkurczewski/springmockk/tree/migration + +# SpringMockK + +Support for Spring Boot integration tests written in Kotlin using [MockK](https://mockk.io/) instead of Mockito. + +Spring Boot provides `@MockBean` and `@SpyBean` annotations for integration tests, which create mock/spy beans using Mockito. + +This project provides equivalent annotations `MockkBean` and `SpykBean` to do the exact same thing with MockK. + +## Principle + +All the Mockito-specific classes of the spring-boot-test library, including the automated tests, have been cloned, translated to Kotlin, and adapted to MockK. + +This library thus provides the same functionality as the standard Mockito-based Spring Boot mock beans. + +For example (using JUnit 5, but you can of course also use JUnit 4): + +```kotlin +@ExtendWith(SpringExtension::class) +@WebMvcTest +class GreetingControllerTest { + @MockkBean + private lateinit var greetingService: GreetingService + + @Autowired + private lateinit var controller: GreetingController + + @Test + fun `should greet by delegating to the greeting service`() { + every { greetingService.greet("John") } returns "Hi John" + + assertThat(controller.greet("John")).isEqualTo("Hi John") + verify { greetingService.greet("John") } + } +} +``` + +## Usage + +### Gradle (Kotlin DSL) + +Add this to your dependencies: +```kotlin +testImplementation("io.mockk:springmockk:4.0.2") +``` + +If you want to make sure Mockito (and the standard `MockBean` and `SpyBean` annotations) is not used, you can also exclude the mockito dependency: +```kotlin +testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "mockito-core") +} +``` + +### Maven + +Add this to your dependencies: +```xml + + io.mockk + springmockk + 4.0.2 + test + +``` + +## Differences with Mockito + + - the MockK defaults are used, which means that mocks created by the annotations are strict (i.e. not relaxed) by default. But [you can configure MockK](https://mockk.io/#settings-file) to use different defaults globally, or you can use `@MockkBean(relaxed = true)` or `@MockkBean(relaxUnitFun = true)`. + - the created mocks can't be serializable as they can be with Mockito (AFAIK, MockK doesn't support that feature) + +## Gotchas + +In some situations, the beans that need to be spied are JDK proxies. In recent versions of Java (Java 16+ AFAIK), +MockK can't spy JDK proxies unless you pass the argument `--add-opens java.base/java.lang.reflect=ALL-UNNAMED` +to the JVM running the tests. + +Not doing that and trying to spy on a JDK proxy will lead to an error such as + +``` +java.lang.IllegalAccessException: class io.mockk.impl.InternalPlatform cannot access a member of class java.lang.reflect.Proxy (in module java.base) with modifiers "protected" +``` + +To pass that option to the test JVM with Gradle, configure the test task with + +```kotlin +tasks.test { + // ... + jvmArgs( + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED" + ) +} +``` + +For Maven users: + +```xml + + org.apache.maven.plugins + maven-surefire-plugin + + + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + + + +```` + +## Limitations + - the [issue 5837](https://github.com/spring-projects/spring-boot/issues/5837), which has been fixed for Mockito spies using Mockito-specific features, also exists with MockK, and hasn't been fixed yet. + If you have a good idea, please tell! + - [this is not an official Spring Boot project](https://github.com/spring-projects/spring-boot/issues/15749), so it might not work out of the box for newest versions if backwards incompatible changes are introduced in Spring Boot. + Please file issues if you find problems. + - annotations are looked up on fields, and not on properties (for now). + This doesn't matter much until you use a custom qualifier annotation. + In that case, make sure that it targets fields and not properties, or use `@field:YourQualifier` to apply it on your beans. + +## Versions compatibility + + - Version 4.x of SpringMockK: compatible with Spring Boot 3.x, Java 17+ + - Version 3.x of SpringMockK: compatible with Spring Boot 2.4.x, 2.5.x and 2.6.x, Java 8+ + - Version 2.x of SpringMockK: compatible with Spring Boot 2.2.x and 2.3.x, Java 8+ + - Version 1.x of SpringMockK: compatible with Spring Boot 2.1.x, Java 8+ + +## How to build + +``` + ./gradlew build +``` diff --git a/modules/springmockk/api/springmockk.api b/modules/springmockk/api/springmockk.api new file mode 100644 index 000000000..7fd611a2b --- /dev/null +++ b/modules/springmockk/api/springmockk.api @@ -0,0 +1,146 @@ +public final class io/mockk/springmockk/ClearMocksTestExecutionListener : org/springframework/test/context/support/AbstractTestExecutionListener { + public fun ()V + public fun afterTestMethod (Lorg/springframework/test/context/TestContext;)V + public fun beforeTestMethod (Lorg/springframework/test/context/TestContext;)V + public fun getOrder ()I +} + +public class io/mockk/springmockk/Definition { + public fun (Ljava/lang/String;Lio/mockk/springmockk/MockkClear;Lio/mockk/springmockk/QualifierDefinition;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getClear ()Lio/mockk/springmockk/MockkClear; + public final fun getName ()Ljava/lang/String; + public final fun getQualifier ()Lio/mockk/springmockk/QualifierDefinition; + public fun hashCode ()I +} + +public final class io/mockk/springmockk/DefinitionsParser { + public fun ()V + public fun (Ljava/util/Collection;)V + public synthetic fun (Ljava/util/Collection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getField (Lio/mockk/springmockk/Definition;)Ljava/lang/reflect/Field; + public final fun getParsedDefinitions ()Ljava/util/Set; + public final fun parse (Ljava/lang/Class;)V +} + +public abstract interface annotation class io/mockk/springmockk/MockkBean : java/lang/annotation/Annotation { + public abstract fun classes ()[Ljava/lang/Class; + public abstract fun clear ()Lio/mockk/springmockk/MockkClear; + public abstract fun extraInterfaces ()[Ljava/lang/Class; + public abstract fun name ()Ljava/lang/String; + public abstract fun relaxUnitFun ()Z + public abstract fun relaxed ()Z + public abstract fun value ()[Ljava/lang/Class; +} + +public abstract interface annotation class io/mockk/springmockk/MockkBeans : java/lang/annotation/Annotation { + public abstract fun value ()[Lio/mockk/springmockk/MockkBean; +} + +public final class io/mockk/springmockk/MockkClear : java/lang/Enum { + public static final field AFTER Lio/mockk/springmockk/MockkClear; + public static final field BEFORE Lio/mockk/springmockk/MockkClear; + public static final field Companion Lio/mockk/springmockk/MockkClear$Companion; + public static final field NONE Lio/mockk/springmockk/MockkClear; + public static fun valueOf (Ljava/lang/String;)Lio/mockk/springmockk/MockkClear; + public static fun values ()[Lio/mockk/springmockk/MockkClear; +} + +public final class io/mockk/springmockk/MockkClear$Companion { + public final fun get (Ljava/lang/Object;)Lio/mockk/springmockk/MockkClear; +} + +public final class io/mockk/springmockk/MockkClearKt { + public static final fun clear (Ljava/lang/Object;Lio/mockk/springmockk/MockkClear;)Ljava/lang/Object; +} + +public final class io/mockk/springmockk/MockkContextCustomizer : org/springframework/test/context/ContextCustomizer { + public fun (Ljava/util/Set;)V + public final fun copy (Ljava/util/Set;)Lio/mockk/springmockk/MockkContextCustomizer; + public static synthetic fun copy$default (Lio/mockk/springmockk/MockkContextCustomizer;Ljava/util/Set;ILjava/lang/Object;)Lio/mockk/springmockk/MockkContextCustomizer; + public fun customizeContext (Lorg/springframework/context/ConfigurableApplicationContext;Lorg/springframework/test/context/MergedContextConfiguration;)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/mockk/springmockk/MockkContextCustomizerFactory : org/springframework/test/context/ContextCustomizerFactory { + public fun ()V + public fun createContextCustomizer (Ljava/lang/Class;Ljava/util/List;)Lorg/springframework/test/context/ContextCustomizer; +} + +public final class io/mockk/springmockk/MockkDefinition : io/mockk/springmockk/Definition { + public fun (Ljava/lang/String;Lorg/springframework/core/ResolvableType;[Lkotlin/reflect/KClass;Lio/mockk/springmockk/MockkClear;ZZLio/mockk/springmockk/QualifierDefinition;)V + public synthetic fun (Ljava/lang/String;Lorg/springframework/core/ResolvableType;[Lkotlin/reflect/KClass;Lio/mockk/springmockk/MockkClear;ZZLio/mockk/springmockk/QualifierDefinition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun createMock ()Ljava/lang/Object; + public final fun createMock (Ljava/lang/String;)Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public final fun getExtraInterfaces ()Ljava/util/Set; + public final fun getRelaxUnitFun ()Z + public final fun getRelaxed ()Z + public final fun getTypeToMock ()Lorg/springframework/core/ResolvableType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/mockk/springmockk/MockkFunctionsKt { + public static final fun isMock (Ljava/lang/Object;)Z +} + +public final class io/mockk/springmockk/MockkPostProcessor : org/springframework/beans/factory/BeanClassLoaderAware, org/springframework/beans/factory/BeanFactoryAware, org/springframework/beans/factory/config/BeanFactoryPostProcessor, org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor, org/springframework/core/Ordered { + public static final field Companion Lio/mockk/springmockk/MockkPostProcessor$Companion; + public fun (Ljava/util/Set;)V + public fun getOrder ()I + public fun postProcessBeanFactory (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)V + public fun postProcessProperties (Lorg/springframework/beans/PropertyValues;Ljava/lang/Object;Ljava/lang/String;)Lorg/springframework/beans/PropertyValues; + public fun setBeanClassLoader (Ljava/lang/ClassLoader;)V + public fun setBeanFactory (Lorg/springframework/beans/factory/BeanFactory;)V +} + +public final class io/mockk/springmockk/MockkPostProcessor$Companion { + public final fun register (Lorg/springframework/beans/factory/support/BeanDefinitionRegistry;Ljava/lang/Class;Ljava/util/Set;)V + public static synthetic fun register$default (Lio/mockk/springmockk/MockkPostProcessor$Companion;Lorg/springframework/beans/factory/support/BeanDefinitionRegistry;Ljava/lang/Class;Ljava/util/Set;ILjava/lang/Object;)V +} + +public final class io/mockk/springmockk/MockkTestExecutionListener : org/springframework/test/context/support/AbstractTestExecutionListener { + public fun ()V + public fun beforeTestMethod (Lorg/springframework/test/context/TestContext;)V + public fun getOrder ()I + public fun prepareTestInstance (Lorg/springframework/test/context/TestContext;)V +} + +public final class io/mockk/springmockk/QualifierDefinition { + public static final field Companion Lio/mockk/springmockk/QualifierDefinition$Companion; + public fun (Ljava/lang/reflect/Field;Ljava/util/Set;)V + public final fun applyTo (Lorg/springframework/beans/factory/support/RootBeanDefinition;)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public final fun matches (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;Ljava/lang/String;)Z +} + +public final class io/mockk/springmockk/QualifierDefinition$Companion { + public final fun forElement (Ljava/lang/reflect/AnnotatedElement;)Lio/mockk/springmockk/QualifierDefinition; +} + +public abstract interface annotation class io/mockk/springmockk/SpykBean : java/lang/annotation/Annotation { + public abstract fun classes ()[Ljava/lang/Class; + public abstract fun clear ()Lio/mockk/springmockk/MockkClear; + public abstract fun name ()Ljava/lang/String; + public abstract fun value ()[Ljava/lang/Class; +} + +public abstract interface annotation class io/mockk/springmockk/SpykBeans : java/lang/annotation/Annotation { + public abstract fun value ()[Lio/mockk/springmockk/SpykBean; +} + +public final class io/mockk/springmockk/SpykDefinition : io/mockk/springmockk/Definition { + public fun (Ljava/lang/String;Lorg/springframework/core/ResolvableType;Lio/mockk/springmockk/MockkClear;Lio/mockk/springmockk/QualifierDefinition;)V + public synthetic fun (Ljava/lang/String;Lorg/springframework/core/ResolvableType;Lio/mockk/springmockk/MockkClear;Lio/mockk/springmockk/QualifierDefinition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun createSpy (Ljava/lang/Object;)Ljava/lang/Object; + public final fun createSpy (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; + public fun equals (Ljava/lang/Object;)Z + public final fun getTypeToSpy ()Lorg/springframework/core/ResolvableType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/modules/springmockk/build.gradle.kts b/modules/springmockk/build.gradle.kts new file mode 100644 index 000000000..6ffe7551a --- /dev/null +++ b/modules/springmockk/build.gradle.kts @@ -0,0 +1,46 @@ +import buildsrc.config.Deps +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.time.Duration + +plugins { + buildsrc.convention.`kotlin-jvm-spring` + buildsrc.convention.`mockk-publishing` +} + +description = "MockBean and SpyBean, but for MockK instead of Mockito" + +val mavenName: String by extra("MockK springmockk") +val mavenDescription: String by extra("${project.description}") + +publishing { + publications { + register("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",