mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5da602bc2 | |||
| 384d2749c6 | |||
| 5150a9a6ad | |||
| a8c5885aff | |||
| 7d844274dc | |||
| 70ceb3abdf | |||
| 9181cce65f | |||
| 8b6523a35b | |||
| e7f349328d | |||
| fe5f5d53b2 | |||
| 033d277bcf | |||
| 227385083d | |||
| d280358e98 | |||
| e08a7ece61 | |||
| 24fa9ea394 | |||
| 4acd9ee69b | |||
| cd1aefa5e0 | |||
| 3c3b8c7aac | |||
| 5a68d1f029 | |||
| ff9b7678a7 | |||
| 799566573a | |||
| 467d5f3ca3 | |||
| 172c25fb0e | |||
| 8771b9ea21 | |||
| ff72652890 | |||
| a1503a59ee | |||
| 5f29df3ac3 | |||
| 25287205ba | |||
| 2bb4df79c3 | |||
| 36fd82f32f | |||
| 0f26f42da7 | |||
| c48fec885c | |||
| 7c4351ab0a | |||
| 07455b10f3 | |||
| a72855b2b3 | |||
| cdddf09c20 | |||
| 53b3b934fc | |||
| 9173e13f74 | |||
| 84be0d85b4 | |||
| c971276f34 | |||
| cd2fbb1ec5 | |||
| e8745522fc | |||
| 2bfb283c91 | |||
| 50b1fb0b15 | |||
| a985b73939 | |||
| 6ec7dcf2c1 | |||
| d94e04d97a | |||
| 5a44897c55 | |||
| 181db1db75 | |||
| 138bdc597d | |||
| ef4f1f0a71 | |||
| 51b89743e1 | |||
| 019f76468b | |||
| f802c0cf24 | |||
| 0da4ae96b4 | |||
| 2ba0022704 | |||
| c59ca087b4 | |||
| 9de2b947cb | |||
| e73e7564d0 | |||
| d927d64c40 | |||
| 8544435833 | |||
| 80ea1f62b6 | |||
| bc287918f2 | |||
| c9436992c9 | |||
| 4350fc21b3 | |||
| 59ed4686c5 | |||
| d5bebd5ced | |||
| 6d86b23fbe | |||
| 0f38c28e91 | |||
| 3505c4bcad | |||
| 58670db46e | |||
| 0cdb8a1657 | |||
| fe8c70dddb | |||
| 1ed74efa6b | |||
| eabd574038 | |||
| ea386c2398 | |||
| 6c6e8ad831 | |||
| 69b74d7645 | |||
| 5ce5647d09 | |||
| ebae02a92b | |||
| 99cb6ba0a7 | |||
| 2baf70768d | |||
| 724700d708 | |||
| 83ba7e69ec | |||
| c1236a3340 | |||
| a942362221 | |||
| 1a34b0dd87 | |||
| b0fcde9aa3 | |||
| a89db89fc0 | |||
| 3d0fffa8e4 | |||
| 71f872e8bb | |||
| 54a90b20ed | |||
| d7a3971403 | |||
| cfa3463cec | |||
| d2264b221a | |||
| 2c32601553 | |||
| ea8b18fbc7 | |||
| 3ab4ee2bba |
@@ -31,10 +31,11 @@ runs:
|
||||
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
|
||||
${{ inputs.java-toolchain == 'true' && '17' || '' }}
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
with:
|
||||
cache-read-only: false
|
||||
develocity-access-key: ${{ inputs.develocity-access-key }}
|
||||
develocity-token-expiry: 4
|
||||
- name: Configure Gradle Properties
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -20,7 +20,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set Up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
|
||||
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
|
||||
- name: Download Release Artifacts
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Deploy
|
||||
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
|
||||
@@ -9,22 +9,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Set Up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: '17'
|
||||
- name: Check Out
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
- name: Build
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build
|
||||
id: build
|
||||
uses: ./.github/actions/build
|
||||
- name: Print JVM Thread Dumps When Cancelled
|
||||
if: cancelled()
|
||||
uses: ./.github/actions/print-jvm-thread-dumps
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
|
||||
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
|
||||
@@ -12,8 +12,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
update-antora-ui-spring:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on Supported Branches
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [ '6.1.x' ]
|
||||
@@ -25,8 +26,9 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
antora-file-path: 'framework-docs/antora-playbook.yml'
|
||||
update-antora-ui-spring-docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on docs-build
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc
|
||||
name: Update
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
|
||||
with:
|
||||
cache-read-only: false
|
||||
- name: Configure Gradle Properties
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=17.0.12-librca
|
||||
java=17.0.13-librca
|
||||
|
||||
+6
-5
@@ -16,7 +16,7 @@ First off, thank you for taking the time to contribute! :+1: :tada:
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc).
|
||||
This project is governed by the [Spring Code of Conduct](https://github.com/spring-projects/spring-framework#coc-ov-file).
|
||||
By participating you are expected to uphold this code.
|
||||
Please report unacceptable behavior to spring-code-of-conduct@spring.io.
|
||||
|
||||
@@ -65,10 +65,6 @@ follow-up reports will need to be created as new issues with a fresh description
|
||||
|
||||
#### Submit a Pull Request
|
||||
|
||||
1. If you have not previously done so, please sign the
|
||||
[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded
|
||||
automatically when you submit the PR.
|
||||
|
||||
1. Should you create an issue first? No, just create the pull request and use the
|
||||
description to provide context and motivation, as you would for an issue. If you want
|
||||
to start a discussion first or have already created an issue, once a pull request is
|
||||
@@ -85,6 +81,11 @@ multiple edits or corrections of the same logical change. See
|
||||
[Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History)
|
||||
for an overview of streamlining the commit history.
|
||||
|
||||
1. All commits must include a _Signed-off-by_ trailer at the end of each commit message
|
||||
to indicate that the contributor agrees to the Developer Certificate of Origin.
|
||||
For additional details, please refer to the blog post
|
||||
[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
|
||||
|
||||
1. Format commit messages using 55 characters for the subject line, 72 characters per line
|
||||
for the description, followed by the issue fixed, for example, `Closes gh-22276`. See the
|
||||
[Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines)
|
||||
|
||||
+4
-4
@@ -25,17 +25,17 @@ configure(allprojects) { project ->
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://repo.spring.io/milestone"
|
||||
url = "https://repo.spring.io/milestone"
|
||||
content {
|
||||
// Netty 5 optional support
|
||||
includeGroup 'io.projectreactor.netty'
|
||||
}
|
||||
}
|
||||
if (version.contains('-')) {
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
maven { url = "https://repo.spring.io/milestone" }
|
||||
}
|
||||
if (version.endsWith('-SNAPSHOT')) {
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
maven { url = "https://repo.spring.io/snapshot" }
|
||||
}
|
||||
}
|
||||
configurations.all {
|
||||
@@ -102,7 +102,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.11.3/api/",
|
||||
// "https://junit.org/junit5/docs/5.11.4/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.20.2");
|
||||
checkstyle.setToolVersion("10.21.1");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -9,7 +9,7 @@ apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ tasks.register('schemaZip', Zip) {
|
||||
archiveClassifier.set("schema")
|
||||
description = "Builds -${archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at https://springframework.org/schema."
|
||||
duplicatesStrategy DuplicatesStrategy.EXCLUDE
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
moduleProjects.each { module ->
|
||||
def Properties schemas = new Properties();
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ javadoc {
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -578,6 +578,8 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: Do not define such beans to be lazy as the `ApplicationContext` will honour that and will not register the method to listen to events.
|
||||
|
||||
The method signature once again declares the event type to which it listens,
|
||||
but, this time, with a flexible name and without implementing a specific listener interface.
|
||||
The event type can also be narrowed through generics as long as the actual event type
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
If a bean is a dependency of another bean, that usually means that one bean is set as a
|
||||
property of another. Typically you accomplish this with the
|
||||
xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`<ref/>` element>]
|
||||
xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`<ref/>` element]
|
||||
in XML-based metadata or through xref:core/beans/dependencies/factory-autowire.adoc[autowiring].
|
||||
|
||||
However, sometimes dependencies between beans are less direct. An example is when a static
|
||||
|
||||
+2
-2
@@ -168,8 +168,8 @@ listings shows how to use the `parent` attribute:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<!-- in the child (descendant) context -->
|
||||
<bean id="accountService" <!-- bean name is the same as the parent bean -->
|
||||
<!-- in the child (descendant) context, bean name is the same as the parent bean -->
|
||||
<bean id="accountService"
|
||||
class="org.springframework.aop.framework.ProxyFactoryBean">
|
||||
<property name="target">
|
||||
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
|
||||
|
||||
@@ -64,7 +64,7 @@ This prevents the same `@Bean` method from accidentally being invoked through a
|
||||
Java method call, which helps to reduce subtle bugs that can be hard to track down.
|
||||
|
||||
When `@Bean` methods are declared within classes that are not annotated with
|
||||
`@Configuration` - or when `@Configuration(proxyBeanMethods=false)` is declared -,
|
||||
`@Configuration`, or when `@Configuration(proxyBeanMethods=false)` is declared,
|
||||
they are referred to as being processed in a "lite" mode. In such scenarios,
|
||||
`@Bean` methods are effectively a general-purpose factory method mechanism without
|
||||
special runtime processing (that is, without generating a CGLIB subclass for it).
|
||||
|
||||
@@ -270,6 +270,7 @@ By default, the following `KeyValues` are created:
|
||||
== HTTP Client Instrumentation
|
||||
|
||||
HTTP client exchange observations are created with the name `"http.client.requests"` for blocking and reactive clients.
|
||||
This observation measures the entire HTTP request/response exchange, from connection establishment up to body deserialization.
|
||||
Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client.
|
||||
|
||||
[[observability.http-client.resttemplate]]
|
||||
@@ -285,7 +286,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
@@ -313,7 +314,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if the request could not be created.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
@@ -342,7 +343,7 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
|
||||
@@ -938,10 +938,10 @@ method parameters:
|
||||
| Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute
|
||||
|
||||
| `@RequestHeader`
|
||||
| Add a request header or multiple headers. The argument may be a `Map<String, ?>` or
|
||||
`MultiValueMap<String, ?>` with multiple headers, a `Collection<?>` of values, or an
|
||||
individual value. Type conversion is supported for non-String values. This overrides
|
||||
the annotation's `headers` attribute.
|
||||
| Add a request header or multiple headers. The argument may be a single value,
|
||||
a `Collection<?>` of values, `Map<String, ?>`,`MultiValueMap<String, ?>`.
|
||||
Type conversion is supported for non-String values. Header values are added and
|
||||
do not override already added header values.
|
||||
|
||||
| `@PathVariable`
|
||||
| Add a variable for expand a placeholder in the request URL. The argument may be a
|
||||
|
||||
@@ -14,16 +14,13 @@ For example, `@Autowired lateinit var thing: Thing` implies that a bean
|
||||
of type `Thing` must be registered in the application context, while `@Autowired lateinit var thing: Thing?`
|
||||
does not raise an error if such a bean does not exist.
|
||||
|
||||
Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)` implies
|
||||
Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, car)` implies
|
||||
that a bean of type `Toy` must be registered in the application context, while a bean of
|
||||
type `Car` may or may not exist. The same behavior applies to autowired constructor parameters.
|
||||
|
||||
NOTE: If you use bean validation on classes with properties or a primary constructor
|
||||
parameters, you may need to use
|
||||
with parameters, you may need to use
|
||||
{kotlin-docs}/annotations.html#annotation-use-site-targets[annotation use-site targets],
|
||||
such as `@field:NotNull` or `@get:Size(min=5, max=15)`, as described in
|
||||
{stackoverflow-site}/a/35853200/1092077[this Stack Overflow response].
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+115
-27
@@ -11,29 +11,21 @@ If multiple candidates match, `@Qualifier` can be provided to narrow the candida
|
||||
override. Alternatively, a candidate whose bean name matches the name of the field will
|
||||
match.
|
||||
|
||||
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
|
||||
exist. However, if you would like for the test to fail when a corresponding bean does not
|
||||
exist, you can set the `enforceOverride` attribute to `true` – for example,
|
||||
`@MockitoBean(enforceOverride = true)`.
|
||||
|
||||
To use a by-name override rather than a by-type override, specify the `name` attribute
|
||||
of the annotation.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Qualifiers, including the name of the field, are used to determine if a separate
|
||||
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
|
||||
the same bean in several tests, make sure to name the field consistently to avoid
|
||||
the same bean in several test classes, make sure to name the field consistently to avoid
|
||||
creating unnecessary contexts.
|
||||
====
|
||||
|
||||
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
|
||||
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
|
||||
|
||||
By default, the `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
|
||||
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
|
||||
If no existing bean matches, a new bean is created on the fly. As mentioned previously,
|
||||
you can switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to
|
||||
`true`.
|
||||
If no existing bean matches, a new bean is created on the fly. However, you can switch to
|
||||
the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the
|
||||
following section for an example.
|
||||
|
||||
The `@MockitoSpyBean` annotation uses the `WRAP`
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
|
||||
@@ -61,6 +53,17 @@ Such fields can therefore be `public`, `protected`, package-private (default vis
|
||||
or `private` depending on the needs or coding practices of the project.
|
||||
====
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
|
||||
== `@MockitoBean` Examples
|
||||
|
||||
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
|
||||
exist. However, if you would like for the test to fail when a corresponding bean does not
|
||||
exist, you can set the `enforceOverride` attribute to `true` – for example,
|
||||
`@MockitoBean(enforceOverride = true)`.
|
||||
|
||||
To use a by-name override rather than a by-type override, specify the `name` (or `value`)
|
||||
attribute of the annotation.
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
|
||||
|
||||
[tabs]
|
||||
@@ -69,11 +72,13 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoBean // <1>
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
|
||||
@@ -82,8 +87,8 @@ Java::
|
||||
In the example above, we are creating a mock for `CustomService`. If more than one bean
|
||||
of that type exists, the bean named `customService` is considered. Otherwise, the test
|
||||
will fail, and you will need to provide a qualifier of some sort to identify which of the
|
||||
`CustomService` beans you want to override. If no such bean exists, a bean definition
|
||||
will be created with an auto-generated bean name.
|
||||
`CustomService` beans you want to override. If no such bean exists, a bean will be
|
||||
created with an auto-generated bean name.
|
||||
|
||||
The following example uses a by-name lookup, rather than a by-type lookup:
|
||||
|
||||
@@ -93,20 +98,43 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoBean("service") // <1>
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
// tests...
|
||||
|
||||
}
|
||||
----
|
||||
<1> Replace the bean named `service` with a Mockito `mock`.
|
||||
======
|
||||
|
||||
If no bean definition named `service` exists, one is created.
|
||||
If no bean named `service` exists, one is created.
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation:
|
||||
`@MockitoBean` can also be used at the type level:
|
||||
|
||||
- on a test class or any superclass or implemented interface in the type hierarchy above
|
||||
the test class
|
||||
- on an enclosing class for a `@Nested` test class or on any class or interface in the
|
||||
type hierarchy or enclosing class hierarchy above the `@Nested` test class
|
||||
|
||||
When `@MockitoBean` is declared at the type level, the type of bean (or beans) to mock
|
||||
must be supplied via the `types` attribute – for example,
|
||||
`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates
|
||||
exist in the application context, you can explicitly specify a bean name to mock by
|
||||
setting the `name` attribute. Note, however, that the `types` attribute must contain a
|
||||
single type if an explicit bean `name` is configured – for example,
|
||||
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
|
||||
|
||||
To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation
|
||||
to create custom _composed annotations_ — for example, to define common mock
|
||||
configuration in a single annotation that can be reused across a test suite.
|
||||
`@MockitoBean` can also be used as a repeatable annotation at the type level — for
|
||||
example, to mock several beans by name.
|
||||
|
||||
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -114,11 +142,70 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MockitoBean(types = {OrderService.class, UserService.class}) // <1>
|
||||
@MockitoBean(name = "ps1", types = PrintingService.class) // <2>
|
||||
public @interface SharedMocks {
|
||||
}
|
||||
----
|
||||
<1> Register `OrderService` and `UserService` mocks by-type.
|
||||
<2> Register `PrintingService` mock by-name.
|
||||
======
|
||||
|
||||
The following demonstrates how `@SharedMocks` can be used on a test class.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
@SharedMocks // <1>
|
||||
class BeanOverrideTests {
|
||||
|
||||
@Autowired OrderService orderService; // <2>
|
||||
|
||||
@Autowired UserService userService; // <2>
|
||||
|
||||
@Autowired PrintingService ps1; // <2>
|
||||
|
||||
// Inject other components that rely on the mocks.
|
||||
|
||||
@Test
|
||||
void testThatDependsOnMocks() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Register common mocks via the custom `@SharedMocks` annotation.
|
||||
<2> Optionally inject mocks to _stub_ or _verify_ them.
|
||||
======
|
||||
|
||||
TIP: The mocks can also be injected into `@Configuration` classes or other test-related
|
||||
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
|
||||
APIs.
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitospybean-examples]]
|
||||
== `@MockitoSpyBean` Examples
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoSpyBean`
|
||||
annotation:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoSpyBean // <1>
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
|
||||
@@ -137,12 +224,13 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoSpyBean("service") // <1>
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean named `service` with a Mockito `spy`.
|
||||
|
||||
+4
-2
@@ -92,8 +92,10 @@ Java::
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Spring searches for the factory method to invoke in the test class, in the test class
|
||||
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class.
|
||||
To locate the factory method to invoke, Spring searches in the class in which the
|
||||
`@TestBean` field is declared, in one of its superclasses, or in any implemented
|
||||
interfaces. If the `@TestBean` field is declared in a `@Nested` test class, the enclosing
|
||||
class hierarchy will also be searched.
|
||||
|
||||
Alternatively, a factory method in an external class can be referenced via its
|
||||
fully-qualified method name following the syntax `<fully-qualified class name>#<method name>`
|
||||
|
||||
@@ -193,7 +193,7 @@ Kotlin::
|
||||
[[webtestclient-fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via
|
||||
This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via
|
||||
mock request and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-cors]]
|
||||
= CORS
|
||||
|
||||
[.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
@@ -364,7 +365,7 @@ Kotlin::
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
|
||||
good fit with <<webflux-fn, functional endpoints>>.
|
||||
good fit with xref:web/webflux-functional.adoc[functional endpoints].
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-fn]]
|
||||
= Functional Endpoints
|
||||
|
||||
[.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-websocket]]
|
||||
= WebSockets
|
||||
|
||||
[.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
This part of the reference documentation covers support for reactive-stack WebSocket
|
||||
|
||||
@@ -605,9 +605,4 @@ For method parameters and returns values, generally, `@HttpExchange` supports a
|
||||
subset of the method parameters that `@RequestMapping` does. Notably, it excludes any
|
||||
server-side specific parameter types. For details, see the list for
|
||||
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and
|
||||
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
|
||||
`@HttpExchange` also supports a `headers()` parameter which accepts `"name=value"`-like
|
||||
pairs like in `@RequestMapping(headers={})` on the client side. On the server side,
|
||||
this extends to the full syntax that
|
||||
xref:#webflux-ann-requestmapping-params-and-headers[`@RequestMapping`] supports.
|
||||
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
@@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models:
|
||||
from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive
|
||||
(Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable
|
||||
difference is that WebFlux also supports reactive `@RequestBody` arguments.
|
||||
* <<webflux-fn>>: Lambda-based, lightweight, and functional programming model. You can think of
|
||||
* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of
|
||||
this as a small library or a set of utilities that an application can use to route and
|
||||
handle requests. The big difference with annotated controllers is that the application
|
||||
is in charge of request handling from start to finish versus declaring intent through
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[mvc-cors]]
|
||||
= CORS
|
||||
|
||||
[.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[webmvc-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
|
||||
|
||||
[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[websocket]]
|
||||
= WebSockets
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
This part of the reference documentation covers support for Servlet stack, WebSocket
|
||||
|
||||
@@ -8,20 +8,20 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.2"))
|
||||
api(platform("io.netty:netty-bom:4.1.115.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.3"))
|
||||
api(platform("io.netty:netty-bom:4.1.117.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.1"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.2"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.4"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.15"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.2"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.16"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.11.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.14.2"))
|
||||
api(platform("org.junit:junit-bom:5.11.4"))
|
||||
api(platform("org.mockito:mockito-bom:5.15.2"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
@@ -31,7 +31,7 @@ dependencies {
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.11.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.28.3")
|
||||
api("com.google.protobuf:protobuf-java-util:4.29.3")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.1
|
||||
version=6.2.2
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -99,7 +99,7 @@ abstract class AbstractProxyExceptionHandlingTests {
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(undeclaredCheckedException);
|
||||
assertThat(throwableSeenByCaller)
|
||||
.isInstanceOf(UndeclaredThrowableException.class)
|
||||
.hasCauseReference(undeclaredCheckedException);
|
||||
.cause().isSameAs(undeclaredCheckedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -147,7 +147,7 @@ abstract class AbstractProxyExceptionHandlingTests {
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller)
|
||||
.isInstanceOf(UndeclaredThrowableException.class)
|
||||
.hasCauseReference(undeclaredCheckedException);
|
||||
.cause().isSameAs(undeclaredCheckedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -95,7 +95,7 @@ public abstract class BeanFactoryAnnotationUtils {
|
||||
// Full qualifier matching supported.
|
||||
return qualifiedBeanOfType(lbf, beanType, qualifier);
|
||||
}
|
||||
else if (beanFactory.containsBean(qualifier)) {
|
||||
else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) {
|
||||
// Fallback: target bean at least found by bean name.
|
||||
return beanFactory.getBean(qualifier, beanType);
|
||||
}
|
||||
@@ -110,16 +110,16 @@ public abstract class BeanFactoryAnnotationUtils {
|
||||
/**
|
||||
* Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a qualifier
|
||||
* (for example, {@code <qualifier>} or {@code @Qualifier}) matching the given qualifier).
|
||||
* @param bf the factory to get the target bean from
|
||||
* @param beanFactory the factory to get the target bean from
|
||||
* @param beanType the type of bean to retrieve
|
||||
* @param qualifier the qualifier for selecting between multiple bean matches
|
||||
* @return the matching bean of type {@code T} (never {@code null})
|
||||
*/
|
||||
private static <T> T qualifiedBeanOfType(ListableBeanFactory bf, Class<T> beanType, String qualifier) {
|
||||
String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(bf, beanType);
|
||||
private static <T> T qualifiedBeanOfType(ListableBeanFactory beanFactory, Class<T> beanType, String qualifier) {
|
||||
String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, beanType);
|
||||
String matchingBean = null;
|
||||
for (String beanName : candidateBeans) {
|
||||
if (isQualifierMatch(qualifier::equals, beanName, bf)) {
|
||||
if (isQualifierMatch(qualifier::equals, beanName, beanFactory)) {
|
||||
if (matchingBean != null) {
|
||||
throw new NoUniqueBeanDefinitionException(beanType, matchingBean, beanName);
|
||||
}
|
||||
@@ -127,11 +127,11 @@ public abstract class BeanFactoryAnnotationUtils {
|
||||
}
|
||||
}
|
||||
if (matchingBean != null) {
|
||||
return bf.getBean(matchingBean, beanType);
|
||||
return beanFactory.getBean(matchingBean, beanType);
|
||||
}
|
||||
else if (bf.containsBean(qualifier)) {
|
||||
else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) {
|
||||
// Fallback: target bean at least found by bean name - probably a manually registered singleton.
|
||||
return bf.getBean(qualifier, beanType);
|
||||
return beanFactory.getBean(qualifier, beanType);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() +
|
||||
|
||||
+6
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -991,7 +991,11 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
*/
|
||||
@Nullable
|
||||
private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
|
||||
this.singletonLock.lock();
|
||||
boolean locked = this.singletonLock.tryLock();
|
||||
if (!locked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName);
|
||||
if (bw != null) {
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
/** Map from bean name to merged BeanDefinitionHolder. */
|
||||
private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256);
|
||||
|
||||
// Set of bean definition names with a primary marker. */
|
||||
/** Set of bean definition names with a primary marker. */
|
||||
private final Set<String> primaryBeanNames = ConcurrentHashMap.newKeySet(16);
|
||||
|
||||
/** Map of singleton and non-singleton bean names, keyed by dependency type. */
|
||||
|
||||
+20
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -279,7 +279,25 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
|
||||
try {
|
||||
beforeSingletonCreation(beanName);
|
||||
}
|
||||
catch (BeanCurrentlyInCreationException ex) {
|
||||
if (locked) {
|
||||
throw ex;
|
||||
}
|
||||
// Try late locking for waiting on specific bean to be finished.
|
||||
this.singletonLock.lock();
|
||||
locked = true;
|
||||
// Singleton object should have appeared in the meantime.
|
||||
singletonObject = this.singletonObjects.get(beanName);
|
||||
if (singletonObject != null) {
|
||||
return singletonObject;
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
}
|
||||
|
||||
boolean newSingleton = false;
|
||||
boolean recordSuppressedExceptions = (locked && this.suppressedExceptions == null);
|
||||
if (recordSuppressedExceptions) {
|
||||
|
||||
+1
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Juergen Hoeller
|
||||
@@ -56,9 +55,6 @@ class BeanFactoryLockingTests {
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Thread thread = new Thread(() -> {
|
||||
// Fail for circular reference from other thread
|
||||
assertThatExceptionOfType(BeanCurrentlyInCreationException.class).isThrownBy(() ->
|
||||
beanFactory.getBean(ThreadDuringInitialization.class));
|
||||
// Leniently create unrelated other bean outside of singleton lock
|
||||
assertThat(beanFactory.getBean(TestBean.class).getName()).isEqualTo("tb");
|
||||
// Creation attempt in other thread was successful
|
||||
|
||||
@@ -1060,7 +1060,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
}
|
||||
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
|
||||
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
|
||||
indexes.forEach(i -> list.add(null));
|
||||
for (int i = 0; i < size; i++) {
|
||||
list.add(null);
|
||||
}
|
||||
for (int index : indexes) {
|
||||
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
|
||||
}
|
||||
@@ -1080,7 +1082,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
}
|
||||
int startIdx = paramPath.length() + 1;
|
||||
int endIdx = name.indexOf(']', startIdx);
|
||||
String nestedPath = name.substring(0, endIdx + 2);
|
||||
String nestedPath = ((name.length() > endIdx + 1) ? name.substring(0, endIdx + 2) : "");
|
||||
boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\'');
|
||||
String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx));
|
||||
if (map == null) {
|
||||
@@ -1114,7 +1116,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
SortedSet<Integer> indexes = null;
|
||||
for (String name : valueResolver.getNames()) {
|
||||
if (name.startsWith(paramPath + "[")) {
|
||||
int endIndex = name.indexOf(']', paramPath.length() + 2);
|
||||
int endIndex = name.indexOf(']', paramPath.length() + 1);
|
||||
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
|
||||
int index = Integer.parseInt(rawIndex);
|
||||
indexes = (indexes != null ? indexes : new TreeSet<>());
|
||||
|
||||
+1
-1
@@ -367,7 +367,7 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
container = null;
|
||||
}
|
||||
|
||||
if (node.getKind().equals(ElementKind.PROPERTY)) {
|
||||
if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
|
||||
nestedViolations
|
||||
.computeIfAbsent(parameterNode, k ->
|
||||
new ParamErrorsBuilder(parameter, value, container, index, key))
|
||||
|
||||
+2
-2
@@ -213,8 +213,8 @@ class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventList
|
||||
|
||||
@Test
|
||||
void invokeListenerWithWrongGenericPayload() {
|
||||
Method method = ReflectionUtils.findMethod
|
||||
(SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class);
|
||||
Method method = ReflectionUtils.findMethod(
|
||||
SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class);
|
||||
EntityWrapper<Integer> payload = new EntityWrapper<>(123);
|
||||
invokeListener(method, new PayloadApplicationEvent<>(this, payload));
|
||||
verify(this.sampleEvents, times(0)).handleGenericStringPayload(any());
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ class ConversionServiceFactoryBeanTests {
|
||||
converters.add(new ConverterFactory<String, Bar>() {
|
||||
@Override
|
||||
public <T extends Bar> Converter<String, T> getConverter(Class<T> targetType) {
|
||||
return new Converter<> () {
|
||||
return new Converter<>() {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T convert(String source) {
|
||||
|
||||
+18
@@ -121,6 +121,24 @@ class DataBinderConstructTests {
|
||||
assertThat(list.get(2).param1()).isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test // gh-34145
|
||||
void listBindingWithNonconsecutiveIndices() {
|
||||
MapValueResolver valueResolver = new MapValueResolver(Map.of(
|
||||
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
|
||||
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
|
||||
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
|
||||
|
||||
DataBinder binder = initDataBinder(ListDataClass.class);
|
||||
binder.construct(valueResolver);
|
||||
|
||||
ListDataClass dataClass = getTarget(binder);
|
||||
List<DataClass> list = dataClass.dataClassList();
|
||||
|
||||
assertThat(list.get(0).param1()).isEqualTo("value1");
|
||||
assertThat(list.get(1).param1()).isEqualTo("value2");
|
||||
assertThat(list.get(3).param1()).isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapBinding() {
|
||||
MapValueResolver valueResolver = new MapValueResolver(Map.of(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -180,7 +180,7 @@ public final class GenericTypeResolver {
|
||||
generics[i] = resolvedTypeArgument;
|
||||
}
|
||||
else {
|
||||
generics[i] = ResolvableType.forType(typeArgument);
|
||||
generics[i] = ResolvableType.forType(typeArgument).resolveType();
|
||||
}
|
||||
}
|
||||
else if (typeArgument instanceof ParameterizedType) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -410,8 +410,9 @@ public class ResolvableType implements Serializable {
|
||||
}
|
||||
matchedBefore.put(this.type, other.type);
|
||||
for (int i = 0; i < ourGenerics.length; i++) {
|
||||
if (!ourGenerics[i].isAssignableFrom(otherGenerics[i],
|
||||
!other.hasUnresolvableGenerics(), matchedBefore, upUntilUnresolvable)) {
|
||||
ResolvableType otherGeneric = otherGenerics[i];
|
||||
if (!ourGenerics[i].isAssignableFrom(otherGeneric,
|
||||
!otherGeneric.isUnresolvableTypeVariable(), matchedBefore, upUntilUnresolvable)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1729,8 +1730,16 @@ public class ResolvableType implements Serializable {
|
||||
* @return {@code true} if these bounds are assignable from all types
|
||||
*/
|
||||
public boolean isAssignableFrom(ResolvableType[] types, @Nullable Map<Type, Type> matchedBefore) {
|
||||
for (ResolvableType type : types) {
|
||||
if (!isAssignableFrom(type, matchedBefore)) {
|
||||
for (ResolvableType bound : this.bounds) {
|
||||
boolean matched = false;
|
||||
for (ResolvableType type : types) {
|
||||
if (this.kind == Kind.UPPER ? bound.isAssignableFrom(type, false, matchedBefore, false) :
|
||||
type.isAssignableFrom(bound, false, matchedBefore, false)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+34
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,6 +40,7 @@ import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableSet;
|
||||
@@ -645,13 +646,15 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
}
|
||||
if (currentPrefix != null) {
|
||||
// A prefix match found, potentially to be turned into a common parent cache entry.
|
||||
if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) {
|
||||
commonPrefix = currentPrefix;
|
||||
existingPath = path;
|
||||
}
|
||||
else if (currentPrefix.equals(commonPrefix)) {
|
||||
commonUnique = false;
|
||||
if (checkPathWithinPackage(path.substring(currentPrefix.length()))) {
|
||||
// A prefix match found, potentially to be turned into a common parent cache entry.
|
||||
if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) {
|
||||
commonPrefix = currentPrefix;
|
||||
existingPath = path;
|
||||
}
|
||||
else if (currentPrefix.equals(commonPrefix)) {
|
||||
commonUnique = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (actualRootPath == null || path.length() > actualRootPath.length()) {
|
||||
@@ -804,7 +807,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
if (separatorIndex == -1) {
|
||||
separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
|
||||
}
|
||||
if (separatorIndex != -1) {
|
||||
if (separatorIndex >= 0) {
|
||||
jarFileUrl = urlFile.substring(0, separatorIndex);
|
||||
rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
|
||||
NavigableSet<String> entriesCache = this.jarEntriesCache.get(jarFileUrl);
|
||||
@@ -831,11 +834,17 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
|
||||
if (con instanceof JarURLConnection jarCon) {
|
||||
// Should usually be the case for traditional JAR files.
|
||||
jarFile = jarCon.getJarFile();
|
||||
jarFileUrl = jarCon.getJarFileURL().toExternalForm();
|
||||
JarEntry jarEntry = jarCon.getJarEntry();
|
||||
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
|
||||
closeJarFile = !jarCon.getUseCaches();
|
||||
try {
|
||||
jarFile = jarCon.getJarFile();
|
||||
jarFileUrl = jarCon.getJarFileURL().toExternalForm();
|
||||
JarEntry jarEntry = jarCon.getJarEntry();
|
||||
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
|
||||
closeJarFile = !jarCon.getUseCaches();
|
||||
}
|
||||
catch (FileNotFoundException ex) {
|
||||
// Happens in case of cached root directory without specific subdirectory present.
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No JarURLConnection -> need to resort to URL file parsing.
|
||||
@@ -872,7 +881,13 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
Set<Resource> result = new LinkedHashSet<>(64);
|
||||
NavigableSet<String> entriesCache = new TreeSet<>();
|
||||
for (String entryPath : jarFile.stream().map(JarEntry::getName).sorted().toList()) {
|
||||
Iterator<String> entryIterator = jarFile.stream().map(JarEntry::getName).sorted().iterator();
|
||||
while (entryIterator.hasNext()) {
|
||||
String entryPath = entryIterator.next();
|
||||
int entrySeparatorIndex = entryPath.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
|
||||
if (entrySeparatorIndex >= 0) {
|
||||
entryPath = entryPath.substring(entrySeparatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
|
||||
}
|
||||
entriesCache.add(entryPath);
|
||||
if (entryPath.startsWith(rootEntryPath)) {
|
||||
String relativePath = entryPath.substring(rootEntryPath.length());
|
||||
@@ -1103,6 +1118,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
return (path.startsWith("/") ? path.substring(1) : path);
|
||||
}
|
||||
|
||||
private static boolean checkPathWithinPackage(String path) {
|
||||
return (path.contains("/") && !path.contains(ResourceUtils.JAR_URL_SEPARATOR));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime.
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,10 +51,11 @@ public class FlightRecorderApplicationStartup implements ApplicationStartup {
|
||||
|
||||
@Override
|
||||
public StartupStep start(String name) {
|
||||
Long parentId = this.currentSteps.getFirst();
|
||||
long sequenceId = this.currentSequenceId.incrementAndGet();
|
||||
this.currentSteps.offerFirst(sequenceId);
|
||||
return new FlightRecorderStartupStep(sequenceId, name,
|
||||
this.currentSteps.getFirst(), committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
|
||||
parentId, committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -519,7 +519,7 @@ final class PlaceholderParser {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
String value = resolveRecursively(resolutionContext, this.key);
|
||||
String value = resolveRecursively(resolutionContext);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
@@ -528,6 +528,17 @@ final class PlaceholderParser {
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolveRecursively(PartResolutionContext resolutionContext) {
|
||||
if (!this.text().equals(this.key)) {
|
||||
String value = resolveRecursively(resolutionContext, this.text());
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return resolveRecursively(resolutionContext, this.key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -719,8 +719,7 @@ public abstract class ReflectionUtils {
|
||||
// Keep backing up the inheritance hierarchy.
|
||||
Class<?> targetClass = clazz;
|
||||
do {
|
||||
Field[] fields = getDeclaredFields(targetClass);
|
||||
for (Field field : fields) {
|
||||
for (Field field : getDeclaredFields(targetClass)) {
|
||||
if (ff != null && !ff.matches(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+2
-2
@@ -148,8 +148,8 @@ class GeneratedClassesTests {
|
||||
"one", TestComponent.class, emptyTypeCustomizer);
|
||||
GeneratedClass generatedClass2 = this.generatedClasses.getOrAddForFeatureComponent(
|
||||
"one", TestComponent.class, emptyTypeCustomizer);
|
||||
GeneratedClass generatedClass3 = prefixed.getOrAddForFeatureComponent
|
||||
("one", TestComponent.class, emptyTypeCustomizer);
|
||||
GeneratedClass generatedClass3 = prefixed.getOrAddForFeatureComponent(
|
||||
"one", TestComponent.class, emptyTypeCustomizer);
|
||||
GeneratedClass generatedClass4 = prefixed.getOrAddForFeatureComponent(
|
||||
"one", TestComponent.class, emptyTypeCustomizer);
|
||||
assertThat(generatedClass1).isSameAs(generatedClass2).isNotSameAs(generatedClass3);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -211,6 +211,14 @@ class GenericTypeResolverTests {
|
||||
assertThat(ResolvableType.forType(resolved).getGeneric(0).getGeneric(0).resolve()).isEqualTo(String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedTypeWithBase() {
|
||||
Type type = method(WithBaseTypes.class, "get").getGenericReturnType();
|
||||
Type resolvedType = resolveType(type, WithBaseTypes.class);
|
||||
ParameterizedTypeReference<List<A>> reference = new ParameterizedTypeReference<>() {};
|
||||
assertThat(resolvedType).isEqualTo(reference.getType());
|
||||
}
|
||||
|
||||
private static Method method(Class<?> target, String methodName, Class<?>... parameterTypes) {
|
||||
Method method = findMethod(target, methodName, parameterTypes);
|
||||
assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull();
|
||||
@@ -398,4 +406,12 @@ class GenericTypeResolverTests {
|
||||
public interface StringListOfListSupplier extends ListOfListSupplier<String> {
|
||||
}
|
||||
|
||||
static class WithBaseTypes {
|
||||
|
||||
<T extends A> List<T> get() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -1189,10 +1189,11 @@ class ResolvableTypeTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAssignableFromForUnresolvedWildcards() {
|
||||
void isAssignableFromForUnresolvedWildcard() {
|
||||
ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>());
|
||||
ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed());
|
||||
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class);
|
||||
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, CharSequence.class);
|
||||
ResolvableType wildcardConsumer = ResolvableType.forInstance(new WildcardConsumer<>());
|
||||
|
||||
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
|
||||
assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue();
|
||||
@@ -1206,6 +1207,38 @@ class ResolvableTypeTests {
|
||||
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcard)).isTrue();
|
||||
assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse();
|
||||
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcardFixed)).isFalse();
|
||||
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFrom(wildcard)).isFalse();
|
||||
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFromResolvedPart(wildcard)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAssignableFromForUnresolvedDoubleWildcard() {
|
||||
ResolvableType wildcard = ResolvableType.forInstance(new DoubleWildcard<>());
|
||||
ResolvableType wildcardFixed = ResolvableType.forInstance(new DoubleWildcardFixed());
|
||||
ResolvableType wildcardConsumer = ResolvableType.forInstance(new DoubleWildcardConsumer<>());
|
||||
|
||||
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
|
||||
assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue();
|
||||
assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse();
|
||||
assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcard)).isFalse();
|
||||
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFrom(wildcard)).isTrue();
|
||||
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFromResolvedPart(wildcard)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void strictGenericsMatching() {
|
||||
ResolvableType consumerUnresolved = ResolvableType.forClass(Consumer.class);
|
||||
ResolvableType consumerObject = ResolvableType.forClassWithGenerics(Consumer.class, Object.class);
|
||||
ResolvableType consumerNestedUnresolved = ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forClass(Consumer.class));
|
||||
|
||||
assertThat(consumerUnresolved.isAssignableFrom(consumerObject)).isTrue();
|
||||
assertThat(consumerUnresolved.isAssignableFromResolvedPart(consumerObject)).isTrue();
|
||||
assertThat(consumerObject.isAssignableFrom(consumerUnresolved)).isTrue();
|
||||
assertThat(consumerObject.isAssignableFromResolvedPart(consumerUnresolved)).isTrue();
|
||||
assertThat(consumerUnresolved.isAssignableFrom(consumerNestedUnresolved)).isTrue();
|
||||
assertThat(consumerUnresolved.isAssignableFromResolvedPart(consumerNestedUnresolved)).isTrue();
|
||||
assertThat(consumerObject.isAssignableFrom(consumerNestedUnresolved)).isFalse();
|
||||
assertThat(consumerObject.isAssignableFromResolvedPart(consumerNestedUnresolved)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1752,12 +1785,28 @@ class ResolvableTypeTests {
|
||||
}
|
||||
|
||||
|
||||
public class Wildcard<T extends Number> {
|
||||
public interface Consumer<T> {
|
||||
}
|
||||
|
||||
public class WildcardFixed extends Wildcard<Integer> {
|
||||
public class Wildcard<T extends CharSequence> {
|
||||
}
|
||||
|
||||
public class WildcardFixed extends Wildcard<String> {
|
||||
}
|
||||
|
||||
public class WildcardConsumer<T extends CharSequence & Serializable> implements Consumer<Wildcard<T>> {
|
||||
}
|
||||
|
||||
public class DoubleWildcard<T extends CharSequence & Serializable> {
|
||||
}
|
||||
|
||||
public class DoubleWildcardFixed extends DoubleWildcard<String> {
|
||||
}
|
||||
|
||||
public class DoubleWildcardConsumer<T extends CharSequence & Serializable> implements Consumer<DoubleWildcard<T>> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface VariableNameSwitch<V, K> extends MultiValueMap<K, V> {
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.util.PlaceholderParser.ParsedValue;
|
||||
import org.springframework.util.PlaceholderParser.TextPart;
|
||||
@@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@@ -220,12 +222,37 @@ class PlaceholderParserTests {
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("exactMatchPlaceholders")
|
||||
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("prefix://my-service", "example-service");
|
||||
properties.setProperty("px", "prefix");
|
||||
properties.setProperty("p1", "${prefix://my-service}");
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> exactMatchPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${prefix://my-service}", "example-service"),
|
||||
Arguments.of("${p1}", "example-service")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithKeyEqualsToText() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "Steve");
|
||||
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
|
||||
.isEqualTo("Steve");
|
||||
verifyPlaceholderResolutions(resolver, "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithHardcodedFallback() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver();
|
||||
assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver))
|
||||
.isEqualTo("Steve");
|
||||
verifyPlaceholderResolutions(resolver, "firstName");
|
||||
verifyPlaceholderResolutions(resolver, "firstName:Steve", "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -233,7 +260,7 @@ class PlaceholderParserTests {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver))
|
||||
.isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
|
||||
verifyPlaceholderResolutions(resolver, "invalid:Name", "invalid", "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -348,8 +375,9 @@ class PlaceholderParserTests {
|
||||
}
|
||||
|
||||
void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
|
||||
InOrder ordered = inOrder(mock);
|
||||
for (String placeholder : placeholders) {
|
||||
verify(mock).resolvePlaceholder(placeholder);
|
||||
ordered.verify(mock).resolvePlaceholder(placeholder);
|
||||
}
|
||||
verifyNoMoreInteractions(mock);
|
||||
}
|
||||
|
||||
+23
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,6 +44,7 @@ class PropertyPlaceholderHelperTests {
|
||||
|
||||
private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}");
|
||||
|
||||
|
||||
@Test
|
||||
void withProperties() {
|
||||
String text = "foo=${foo}";
|
||||
@@ -116,8 +117,8 @@ class PropertyPlaceholderHelperTests {
|
||||
props.setProperty("foo", "bar");
|
||||
|
||||
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, null, false);
|
||||
assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
|
||||
helper.replacePlaceholders(text, props));
|
||||
assertThatExceptionOfType(PlaceholderResolutionException.class)
|
||||
.isThrownBy(() -> helper.replacePlaceholders(text, props));
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -153,13 +154,30 @@ class PropertyPlaceholderHelperTests {
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("exactMatchPlaceholders")
|
||||
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("prefix://my-service", "example-service");
|
||||
properties.setProperty("px", "prefix");
|
||||
properties.setProperty("p1", "${prefix://my-service}");
|
||||
assertThat(this.helper.replacePlaceholders(text, properties)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> exactMatchPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${prefix://my-service}", "example-service"),
|
||||
Arguments.of("${p1}", "example-service")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
|
||||
|
||||
private static PlaceholderResolver mockPlaceholderResolver(String... pairs) {
|
||||
if (pairs.length % 2 == 1) {
|
||||
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
|
||||
}
|
||||
PlaceholderResolver resolver = mock(PlaceholderResolver.class);
|
||||
PlaceholderResolver resolver = mock();
|
||||
for (int i = 0; i < pairs.length; i += 2) {
|
||||
String key = pairs[i];
|
||||
String value = pairs[i + 1];
|
||||
@@ -168,5 +186,4 @@ class PropertyPlaceholderHelperTests {
|
||||
return resolver;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -229,8 +229,9 @@ public class FunctionReference extends SpelNodeImpl {
|
||||
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
|
||||
|
||||
if (isSuspectedVarargs) {
|
||||
if (declaredParamCount == 1) {
|
||||
// We only repackage the varargs if it is the ONLY argument -- for example,
|
||||
if (declaredParamCount == 1 && !methodHandle.isVarargsCollector()) {
|
||||
// We only repackage the arguments if the MethodHandle accepts a single
|
||||
// argument AND the MethodHandle is not a "varargs collector" -- for example,
|
||||
// when we are dealing with a bound MethodHandle.
|
||||
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
|
||||
methodHandle.type().parameterArray(), functionArgs);
|
||||
|
||||
+6
-1
@@ -108,11 +108,16 @@ class TestScenarioCreator {
|
||||
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
|
||||
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
|
||||
|
||||
// #formatObjectVarargs(format, args...)
|
||||
// #formatPrimitiveVarargs(format, args...)
|
||||
MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
|
||||
"formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class));
|
||||
testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs);
|
||||
|
||||
// #varargsFunctionHandle(args...)
|
||||
MethodHandle varargsFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
|
||||
"varargsFunction", MethodType.methodType(String.class, String[].class));
|
||||
testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle);
|
||||
|
||||
// #add(int, int)
|
||||
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
|
||||
"add", MethodType.methodType(int.class, int.class, int.class));
|
||||
|
||||
+23
@@ -79,6 +79,8 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
|
||||
@Test
|
||||
void functionWithVarargs() {
|
||||
// static String varargsFunction(String... strings) -> Arrays.toString(strings)
|
||||
|
||||
evaluate("#varargsFunction()", "[]", String.class);
|
||||
evaluate("#varargsFunction(new String[0])", "[]", String.class);
|
||||
evaluate("#varargsFunction('a')", "[a]", String.class);
|
||||
@@ -241,6 +243,27 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
evaluate("#formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class);
|
||||
}
|
||||
|
||||
@Test // gh-34109
|
||||
void functionViaMethodHandleForStaticMethodThatAcceptsOnlyVarargs() {
|
||||
// #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings)
|
||||
|
||||
evaluate("#varargsFunctionHandle()", "[]", String.class);
|
||||
evaluate("#varargsFunctionHandle(new String[0])", "[]", String.class);
|
||||
evaluate("#varargsFunctionHandle('a')", "[a]", String.class);
|
||||
evaluate("#varargsFunctionHandle('a','b','c')", "[a, b, c]", String.class);
|
||||
evaluate("#varargsFunctionHandle(new String[]{'a','b','c'})", "[a, b, c]", String.class);
|
||||
// Conversion from int to String
|
||||
evaluate("#varargsFunctionHandle(25)", "[25]", String.class);
|
||||
evaluate("#varargsFunctionHandle('b',25)", "[b, 25]", String.class);
|
||||
evaluate("#varargsFunctionHandle(new int[]{1, 2, 3})", "[1, 2, 3]", String.class);
|
||||
// Strings that contain a comma
|
||||
evaluate("#varargsFunctionHandle('a,b')", "[a,b]", String.class);
|
||||
evaluate("#varargsFunctionHandle('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
|
||||
// null values
|
||||
evaluate("#varargsFunctionHandle(null)", "[null]", String.class);
|
||||
evaluate("#varargsFunctionHandle('a',null,'b')", "[a, null, b]", String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionMethodMustBeStatic() throws Exception {
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
+28
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,8 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@@ -66,6 +68,9 @@ public abstract class AbstractJdbcCall {
|
||||
/** List of RefCursor/ResultSet RowMapper objects. */
|
||||
private final Map<String, RowMapper<?>> declaredRowMappers = new LinkedHashMap<>();
|
||||
|
||||
/** Lock for the compilation step. */
|
||||
private final Lock compilationLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* Has this operation been compiled? Compilation means at least checking
|
||||
* that a DataSource or JdbcTemplate has been provided.
|
||||
@@ -284,24 +289,30 @@ public abstract class AbstractJdbcCall {
|
||||
* @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't
|
||||
* been correctly initialized, for example if no DataSource has been provided
|
||||
*/
|
||||
public final synchronized void compile() throws InvalidDataAccessApiUsageException {
|
||||
if (!isCompiled()) {
|
||||
if (getProcedureName() == null) {
|
||||
throw new InvalidDataAccessApiUsageException("Procedure or Function name is required");
|
||||
}
|
||||
try {
|
||||
this.jdbcTemplate.afterPropertiesSet();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new InvalidDataAccessApiUsageException(ex.getMessage());
|
||||
}
|
||||
compileInternal();
|
||||
this.compiled = true;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") +
|
||||
" [" + getProcedureName() + "] compiled");
|
||||
public final void compile() throws InvalidDataAccessApiUsageException {
|
||||
this.compilationLock.lock();
|
||||
try {
|
||||
if (!isCompiled()) {
|
||||
if (getProcedureName() == null) {
|
||||
throw new InvalidDataAccessApiUsageException("Procedure or Function name is required");
|
||||
}
|
||||
try {
|
||||
this.jdbcTemplate.afterPropertiesSet();
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new InvalidDataAccessApiUsageException(ex.getMessage());
|
||||
}
|
||||
compileInternal();
|
||||
this.compiled = true;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") +
|
||||
" [" + getProcedureName() + "] compiled");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.compilationLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -183,6 +183,13 @@ public abstract class JdbcTransactionObjectSupport implements SavepointManager,
|
||||
catch (SQLFeatureNotSupportedException ex) {
|
||||
// typically on Oracle - ignore
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
// ignore Microsoft SQLServerException: This operation is not supported.
|
||||
String msg = ex.getMessage();
|
||||
if (msg == null || !msg.contains("not supported")) {
|
||||
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
|
||||
}
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class CompositeMessageCondition implements MessageCondition<CompositeMess
|
||||
checkCompatible(other);
|
||||
List<MessageCondition<?>> otherConditions = other.getMessageConditions();
|
||||
for (int i = 0; i < this.messageConditions.size(); i++) {
|
||||
int result = compare (this.messageConditions.get(i), otherConditions.get(i), message);
|
||||
int result = compare(this.messageConditions.get(i), otherConditions.get(i), message);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ public abstract class SharedEntityManagerCreator {
|
||||
@SuppressWarnings("serial")
|
||||
private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class);
|
||||
|
||||
private final EntityManagerFactory targetFactory;
|
||||
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -82,6 +82,9 @@ import org.springframework.lang.Nullable;
|
||||
* <li>{@link ActiveProfiles @ActiveProfiles}</li>
|
||||
* <li>{@link TestPropertySource @TestPropertySource}</li>
|
||||
* <li>{@link DynamicPropertySource @DynamicPropertySource}</li>
|
||||
* <li>{@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}</li>
|
||||
* <li>{@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}</li>
|
||||
* <li>{@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}</li>
|
||||
* <li>{@link org.springframework.test.annotation.DirtiesContext @DirtiesContext}</li>
|
||||
* <li>{@link org.springframework.transaction.annotation.Transactional @Transactional}</li>
|
||||
* <li>{@link org.springframework.test.annotation.Rollback @Rollback}</li>
|
||||
|
||||
+8
-4
@@ -25,15 +25,19 @@ import java.lang.annotation.Target;
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
|
||||
/**
|
||||
* Mark a composed annotation as eligible for Bean Override processing.
|
||||
* Mark a <em>composed annotation</em> as eligible for Bean Override processing.
|
||||
*
|
||||
* <p>Specifying this annotation registers the configured {@link BeanOverrideProcessor}
|
||||
* which must be capable of handling the composed annotation and its attributes.
|
||||
*
|
||||
* <p>Since the composed annotation should only be applied to non-static fields, it is
|
||||
* expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
|
||||
* <p>Since the composed annotation will typically only be applied to non-static
|
||||
* fields, it is expected that the composed annotation is meta-annotated with
|
||||
* {@link Target @Target(ElementType.FIELD)}. However, certain bean override
|
||||
* annotations may be declared with an additional {@code ElementType.TYPE} target
|
||||
* for use at the type level, as is the case for {@code @MockitoBean} which can
|
||||
* be declared on a field, test class, or test interface.
|
||||
*
|
||||
* <p>For concrete examples, see
|
||||
* <p>For concrete examples of such composed annotations, see
|
||||
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
|
||||
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, and
|
||||
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}.
|
||||
|
||||
+39
-31
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -104,11 +104,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
Set<String> generatedBeanNames) {
|
||||
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
|
||||
Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \
|
||||
To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted(
|
||||
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """
|
||||
Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \
|
||||
To override the bean created by the FactoryBean, remove the '&' prefix."""
|
||||
.formatted(beanName, forField(handler.getField())));
|
||||
|
||||
switch (handler.getStrategy()) {
|
||||
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
|
||||
@@ -134,7 +133,6 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
// 4) Create bean by-name, with a provided name
|
||||
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
BeanDefinition existingBeanDefinition = null;
|
||||
if (beanName == null) {
|
||||
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
|
||||
@@ -169,11 +167,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
}
|
||||
else if (requireExistingBean) {
|
||||
throw new IllegalStateException("""
|
||||
Unable to replace bean: there is no bean with name '%s' and type %s \
|
||||
(as required by field '%s.%s')."""
|
||||
.formatted(beanName, handler.getBeanType(),
|
||||
field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
Field field = handler.getField();
|
||||
throw new IllegalStateException(
|
||||
"Unable to replace bean: there is no bean with name '%s' and type %s%s."
|
||||
.formatted(beanName, handler.getBeanType(), requiredByField(field)));
|
||||
}
|
||||
// 4) We are creating a bean by-name with the provided beanName.
|
||||
}
|
||||
@@ -264,13 +261,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
else {
|
||||
String message = "Unable to select a bean to wrap: ";
|
||||
if (candidateCount == 0) {
|
||||
message += "there are no beans of type %s (as required by field '%s.%s')."
|
||||
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field));
|
||||
}
|
||||
else {
|
||||
message += "found %d beans of type %s (as required by field '%s.%s'): %s"
|
||||
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), candidateNames);
|
||||
message += "found %d beans of type %s%s: %s"
|
||||
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
|
||||
}
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
@@ -281,11 +276,9 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
// We are wrapping an existing bean by-name.
|
||||
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
|
||||
if (!candidates.contains(beanName)) {
|
||||
throw new IllegalStateException("""
|
||||
Unable to wrap bean: there is no bean with name '%s' and type %s \
|
||||
(as required by field '%s.%s')."""
|
||||
.formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName()));
|
||||
throw new IllegalStateException(
|
||||
"Unable to wrap bean: there is no bean with name '%s' and type %s%s."
|
||||
.formatted(beanName, beanType, requiredByField(field)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,8 +301,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
else if (candidateCount == 0) {
|
||||
if (requireExistingBean) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
|
||||
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
"Unable to override bean: there are no beans of type %s%s."
|
||||
.formatted(beanType, requiredByField(field)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -320,14 +313,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s"
|
||||
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), candidateNames));
|
||||
"Unable to select a bean to override: found %d beans of type %s%s: %s"
|
||||
.formatted(candidateCount, beanType, requiredByField(field), candidateNames));
|
||||
}
|
||||
|
||||
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
|
||||
boolean checkAutowiredCandidate) {
|
||||
|
||||
Field field = handler.getField();
|
||||
ResolvableType resolvableType = handler.getBeanType();
|
||||
Class<?> type = resolvableType.toClass();
|
||||
|
||||
@@ -345,16 +338,16 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
|
||||
// Filter out non-matching autowire candidates.
|
||||
if (checkAutowiredCandidate) {
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true);
|
||||
if (field != null && checkAutowiredCandidate) {
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(field, true);
|
||||
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
|
||||
}
|
||||
// Filter out scoped proxy targets.
|
||||
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
|
||||
|
||||
// In case of multiple matches, fall back on the field's name as a last resort.
|
||||
if (beanNames.size() > 1) {
|
||||
String fieldName = handler.getField().getName();
|
||||
if (field != null && beanNames.size() > 1) {
|
||||
String fieldName = field.getName();
|
||||
if (beanNames.contains(fieldName)) {
|
||||
return Set.of(fieldName);
|
||||
}
|
||||
@@ -452,4 +445,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
dlbf.destroySingleton(beanName);
|
||||
}
|
||||
|
||||
private static String forField(@Nullable Field field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
}
|
||||
|
||||
private static String requiredByField(@Nullable Field field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
return " (as required by field '%s.%s')".formatted(
|
||||
field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,7 +25,7 @@ import org.springframework.test.context.MergedContextConfiguration;
|
||||
|
||||
/**
|
||||
* {@link ContextCustomizer} implementation that registers the necessary
|
||||
* infrastructure to support {@linkplain BeanOverride bean overriding}.
|
||||
* infrastructure to support {@linkplain BeanOverride Bean Overrides}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Stephane Nicoll
|
||||
|
||||
+5
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,12 +23,11 @@ import java.util.Set;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizerFactory;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ContextCustomizerFactory} implementation that provides support for
|
||||
* Bean Overriding.
|
||||
* {@linkplain BeanOverride Bean Overrides}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Stephane Nicoll
|
||||
@@ -44,18 +43,15 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
List<ContextConfigurationAttributes> configAttributes) {
|
||||
|
||||
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
|
||||
findBeanOverrideHandler(testClass, handlers);
|
||||
findBeanOverrideHandlers(testClass, handlers);
|
||||
if (handlers.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new BeanOverrideContextCustomizer(handlers);
|
||||
}
|
||||
|
||||
private void findBeanOverrideHandler(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
|
||||
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
|
||||
findBeanOverrideHandler(testClass.getEnclosingClass(), handlers);
|
||||
}
|
||||
BeanOverrideHandler.forTestClass(testClass).forEach(handler ->
|
||||
private void findBeanOverrideHandlers(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
|
||||
BeanOverrideHandler.findAllHandlers(testClass).forEach(handler ->
|
||||
Assert.state(handlers.add(handler), () ->
|
||||
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
|
||||
.formatted(testClass.getName(), handler)));
|
||||
|
||||
+139
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,16 +17,18 @@
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
@@ -36,6 +38,7 @@ import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
@@ -51,12 +54,14 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
|
||||
* unique set of metadata used to identify the bean to override. Overridden
|
||||
* {@code equals()} and {@code hashCode()} methods should also delegate to the
|
||||
* {@code super} implementations in this class in order to support the basic
|
||||
* metadata used by all bean overrides.
|
||||
* metadata used by all bean overrides. In addition, it is recommended that
|
||||
* implementations override {@code toString()} to include all relevant metadata
|
||||
* in order to enhance diagnostics.
|
||||
*
|
||||
* <p>Concrete implementations of {@code BeanOverrideHandler} can store additional
|
||||
* metadata to use during override {@linkplain #createOverrideInstance instance
|
||||
* creation} — for example, based on further processing of the annotation
|
||||
* or the annotated field.
|
||||
* creation} — for example, based on further processing of the annotation,
|
||||
* the annotated field, or the annotated class.
|
||||
*
|
||||
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
|
||||
* Any attempt to override a non-singleton bean will result in an exception.
|
||||
@@ -68,9 +73,14 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
|
||||
*/
|
||||
public abstract class BeanOverrideHandler {
|
||||
|
||||
private static final Comparator<MergedAnnotation<? extends Annotation>> reversedMetaDistance =
|
||||
Comparator.<MergedAnnotation<? extends Annotation>> comparingInt(MergedAnnotation::getDistance).reversed();
|
||||
|
||||
|
||||
@Nullable
|
||||
private final Field field;
|
||||
|
||||
private final Set<Annotation> fieldAnnotations;
|
||||
private final Set<Annotation> qualifierAnnotations;
|
||||
|
||||
private final ResolvableType beanType;
|
||||
|
||||
@@ -80,11 +90,11 @@ public abstract class BeanOverrideHandler {
|
||||
private final BeanOverrideStrategy strategy;
|
||||
|
||||
|
||||
protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName,
|
||||
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
|
||||
BeanOverrideStrategy strategy) {
|
||||
|
||||
this.field = field;
|
||||
this.fieldAnnotations = annotationSet(field);
|
||||
this.qualifierAnnotations = getQualifierAnnotations(field);
|
||||
this.beanType = beanType;
|
||||
this.beanName = beanName;
|
||||
this.strategy = strategy;
|
||||
@@ -93,39 +103,117 @@ public abstract class BeanOverrideHandler {
|
||||
/**
|
||||
* Process the given {@code testClass} and build the corresponding
|
||||
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
|
||||
* fields in the test class, its type hierarchy, and its enclosing class hierarchy.
|
||||
* fields in the test class and its type hierarchy.
|
||||
* <p>This method does not search the enclosing class hierarchy and does not
|
||||
* search for {@code @BeanOverride} declarations on classes or interfaces.
|
||||
* @param testClass the test class to process
|
||||
* @return a list of bean override handlers
|
||||
* @see #findAllHandlers(Class)
|
||||
*/
|
||||
public static List<BeanOverrideHandler> forTestClass(Class<?> testClass) {
|
||||
List<BeanOverrideHandler> handlers = new LinkedList<>();
|
||||
ReflectionUtils.doWithFields(testClass, field -> processField(field, testClass, handlers));
|
||||
return findHandlers(testClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given {@code testClass} and build the corresponding
|
||||
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
|
||||
* fields in the test class and in its type hierarchy as well as from
|
||||
* {@code @BeanOverride} declarations on classes and interfaces.
|
||||
* <p>This method additionally searches for {@code @BeanOverride} declarations
|
||||
* in the enclosing class hierarchy based on
|
||||
* {@link TestContextAnnotationUtils#searchEnclosingClass(Class)} semantics.
|
||||
* @param testClass the test class to process
|
||||
* @return a list of bean override handlers
|
||||
* @since 6.2.2
|
||||
*/
|
||||
static List<BeanOverrideHandler> findAllHandlers(Class<?> testClass) {
|
||||
return findHandlers(testClass, false);
|
||||
}
|
||||
|
||||
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
findHandlers(testClass, testClass, handlers, localFieldsOnly);
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find handlers using tail recursion to ensure that "locally declared" bean overrides
|
||||
* take precedence over inherited bean overrides.
|
||||
* <p>Note: the search algorithm is effectively the inverse of the algorithm used in
|
||||
* {@link org.springframework.test.context.TestContextAnnotationUtils#findAnnotationDescriptor(Class, Class)},
|
||||
* but with tail recursion the semantics should be the same.
|
||||
* @param clazz the class in/on which to search
|
||||
* @param testClass the original test class
|
||||
* @param handlers the list of handlers found
|
||||
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
|
||||
* @since 6.2.2
|
||||
*/
|
||||
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
|
||||
boolean localFieldsOnly) {
|
||||
|
||||
// 1) Search enclosing class hierarchy.
|
||||
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
|
||||
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
|
||||
}
|
||||
|
||||
// 2) Search class hierarchy.
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
if (superclass != null && superclass != Object.class) {
|
||||
findHandlers(superclass, testClass, handlers, localFieldsOnly);
|
||||
}
|
||||
|
||||
if (!localFieldsOnly) {
|
||||
// 3) Search interfaces.
|
||||
for (Class<?> ifc : clazz.getInterfaces()) {
|
||||
findHandlers(ifc, testClass, handlers, localFieldsOnly);
|
||||
}
|
||||
|
||||
// 4) Process current class.
|
||||
processClass(clazz, testClass, handlers);
|
||||
}
|
||||
|
||||
// 5) Process fields in current class.
|
||||
ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers));
|
||||
}
|
||||
|
||||
private static void processClass(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers) {
|
||||
processElement(clazz, testClass, (processor, composedAnnotation) ->
|
||||
processor.createHandlers(composedAnnotation, testClass).forEach(handlers::add));
|
||||
}
|
||||
|
||||
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
|
||||
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
|
||||
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
|
||||
processElement(field, testClass, (processor, composedAnnotation) -> {
|
||||
Assert.state(!Modifier.isStatic(field.getModifiers()),
|
||||
() -> "@BeanOverride field must not be static: " + field);
|
||||
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
|
||||
Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
|
||||
|
||||
BeanOverride beanOverride = mergedAnnotation.synthesize();
|
||||
BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
|
||||
Annotation composedAnnotation = metaSource.synthesize();
|
||||
|
||||
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
|
||||
() -> "Multiple @BeanOverride annotations found on field: " + field);
|
||||
BeanOverrideHandler handler = processor.createHandler(composedAnnotation, testClass, field);
|
||||
handlers.add(handler);
|
||||
handlers.add(processor.createHandler(composedAnnotation, testClass, field));
|
||||
});
|
||||
}
|
||||
|
||||
private static void processElement(AnnotatedElement element, Class<?> testClass,
|
||||
BiConsumer<BeanOverrideProcessor, Annotation> consumer) {
|
||||
|
||||
MergedAnnotations.from(element, DIRECT)
|
||||
.stream(BeanOverride.class)
|
||||
.sorted(reversedMetaDistance)
|
||||
.forEach(mergedAnnotation -> {
|
||||
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
|
||||
Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
|
||||
|
||||
BeanOverride beanOverride = mergedAnnotation.synthesize();
|
||||
BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
|
||||
Annotation composedAnnotation = metaSource.synthesize();
|
||||
consumer.accept(processor, composedAnnotation);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the annotated {@link Field}.
|
||||
*/
|
||||
@Nullable
|
||||
public final Field getField() {
|
||||
return this.field;
|
||||
}
|
||||
@@ -222,20 +310,25 @@ public abstract class BeanOverrideHandler {
|
||||
!Objects.equals(this.strategy, that.strategy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// by-name lookup
|
||||
if (this.beanName != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// by-type lookup
|
||||
return (Objects.equals(this.field.getName(), that.field.getName()) &&
|
||||
this.fieldAnnotations.equals(that.fieldAnnotations));
|
||||
if (this.field == null) {
|
||||
return (that.field == null);
|
||||
}
|
||||
return (that.field != null && this.field.getName().equals(that.field.getName()) &&
|
||||
this.qualifierAnnotations.equals(that.qualifierAnnotations));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy);
|
||||
return (this.beanName != null ? hash : hash +
|
||||
Objects.hash(this.field.getName(), this.fieldAnnotations));
|
||||
Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,9 +341,27 @@ public abstract class BeanOverrideHandler {
|
||||
.toString();
|
||||
}
|
||||
|
||||
private static Set<Annotation> annotationSet(Field field) {
|
||||
Annotation[] annotations = field.getAnnotations();
|
||||
return (annotations.length != 0 ? new HashSet<>(Arrays.asList(annotations)) : Collections.emptySet());
|
||||
|
||||
private static Set<Annotation> getQualifierAnnotations(@Nullable Field field) {
|
||||
if (field == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Annotation[] candidates = field.getDeclaredAnnotations();
|
||||
if (candidates.length == 0) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<Annotation> annotations = new HashSet<>(candidates.length - 1);
|
||||
for (Annotation candidate : candidates) {
|
||||
// Assume all non-BeanOverride annotations are "qualifiers".
|
||||
if (!isBeanOverrideAnnotation(candidate.annotationType())) {
|
||||
annotations.add(candidate);
|
||||
}
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private static boolean isBeanOverrideAnnotation(Class<? extends Annotation> type) {
|
||||
return MergedAnnotations.from(type).isPresent(BeanOverride.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,10 +18,13 @@ package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Strategy interface for Bean Override processing, which creates a
|
||||
* {@link BeanOverrideHandler} that drives how the target bean is overridden.
|
||||
* Strategy interface for Bean Override processing, which creates
|
||||
* {@link BeanOverrideHandler} instances that drive how target beans are
|
||||
* overridden.
|
||||
*
|
||||
* <p>At least one composed annotation that is meta-annotated with
|
||||
* {@link BeanOverride @BeanOverride} must be a companion of this processor and
|
||||
@@ -36,17 +39,45 @@ import java.lang.reflect.Field;
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface BeanOverrideProcessor {
|
||||
|
||||
/**
|
||||
* Create a {@link BeanOverrideHandler} for the given annotated field.
|
||||
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
|
||||
* annotation is declared on a field — for example, if the supplied field
|
||||
* is annotated with {@code @MockitoBean}.
|
||||
* @param overrideAnnotation the composed annotation that declares the
|
||||
* {@link BeanOverride @BeanOverride} annotation which registers this processor
|
||||
* {@code @BeanOverride} annotation which registers this processor
|
||||
* @param testClass the test class to process
|
||||
* @param field the annotated field
|
||||
* @return the {@code BeanOverrideHandler} that should handle the given field
|
||||
* @see #createHandlers(Annotation, Class)
|
||||
*/
|
||||
BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field);
|
||||
|
||||
/**
|
||||
* Create a list of {@link BeanOverrideHandler} instances for the given override
|
||||
* annotation and test class.
|
||||
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
|
||||
* annotation is declared at the type level — for example, if the supplied
|
||||
* test class is annotated with {@code @MockitoBean}.
|
||||
* <p>Note that the test class may not be directly annotated with the override
|
||||
* annotation. For example, the override annotation may have been declared
|
||||
* on an interface, superclass, or enclosing class within the test class
|
||||
* hierarchy.
|
||||
* <p>The default implementation returns an empty list, signaling that this
|
||||
* {@code BeanOverrideProcessor} does not support type-level {@code @BeanOverride}
|
||||
* declarations. Can be overridden by concrete implementations to support
|
||||
* type-level use cases.
|
||||
* @param overrideAnnotation the composed annotation that declares the
|
||||
* {@code @BeanOverride} annotation which registers this processor
|
||||
* @param testClass the test class to process
|
||||
* @return the list of {@code BeanOverrideHandlers} for the annotated class
|
||||
* @since 6.2.2
|
||||
* @see #createHandler(Annotation, Class, Field)
|
||||
*/
|
||||
default List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -108,10 +108,11 @@ class BeanOverrideRegistry {
|
||||
}
|
||||
|
||||
void inject(Object target, BeanOverrideHandler handler) {
|
||||
Field field = handler.getField();
|
||||
Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler);
|
||||
String beanName = this.handlerToBeanNameMap.get(handler);
|
||||
Assert.state(StringUtils.hasLength(beanName),
|
||||
() -> "No bean found for BeanOverrideHandler: " + handler);
|
||||
inject(handler.getField(), target, beanName);
|
||||
Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler);
|
||||
inject(field, target, beanName);
|
||||
}
|
||||
|
||||
private void inject(Field field, Object target, String beanName) {
|
||||
|
||||
+24
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,24 +44,24 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* you can set the {@link #enforceOverride() enforceOverride} attribute to {@code true}
|
||||
* — for example, {@code @TestBean(enforceOverride = true)}.
|
||||
*
|
||||
* <p>The instance is created from a zero-argument static factory method in the
|
||||
* test class whose return type is compatible with the annotated field. In the
|
||||
* case of a nested test class, the enclosing class hierarchy is also searched.
|
||||
* Similarly, if the test class extends from a base class or implements any
|
||||
* interfaces, the entire type hierarchy is searched. Alternatively, a factory
|
||||
* method in an external class can be referenced via its fully-qualified method
|
||||
* name following the syntax {@code <fully-qualified class name>#<method name>}
|
||||
* — for example,
|
||||
* <p>The instance is created from a zero-argument static factory method whose
|
||||
* return type is compatible with the annotated field. The factory method can be
|
||||
* declared directly in the class which declares the {@code @TestBean} field or
|
||||
* within the type hierarchy above that class, including implemented interfaces.
|
||||
* If the {@code @TestBean} field is declared in a nested test class, the enclosing
|
||||
* class hierarchy is also searched. Alternatively, a factory method in an external
|
||||
* class can be referenced via its fully-qualified method name following the syntax
|
||||
* {@code <fully-qualified class name>#<method name>} — for example,
|
||||
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
|
||||
*
|
||||
* <p>The factory method is deduced as follows.
|
||||
*
|
||||
* <ul>
|
||||
* <li>If the {@link #methodName()} is specified, look for a static method with
|
||||
* that name.</li>
|
||||
* <li>If a method name is not specified, look for exactly one static method
|
||||
* named with either the name of the annotated field or the name of the bean
|
||||
* (if specified).</li>
|
||||
* <li>If the {@link #methodName methodName} is specified, Spring looks for a static
|
||||
* method with that name.</li>
|
||||
* <li>If a method name is not specified, Spring looks for exactly one static method
|
||||
* whose name is either the name of the annotated field or the {@link #name() name}
|
||||
* of the bean (if specified).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Consider the following example.
|
||||
@@ -110,6 +110,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* {@code protected}, package-private (default visibility), or {@code private}
|
||||
* depending on the needs or coding practices of the project.
|
||||
*
|
||||
* <p>{@code @TestBean} fields will be inherited from an enclosing test class by default. See
|
||||
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* for details.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
@@ -146,15 +150,17 @@ public @interface TestBean {
|
||||
/**
|
||||
* Name of the static factory method that will be used to instantiate the bean
|
||||
* to override.
|
||||
* <p>A search will be performed to find the factory method in the test class,
|
||||
* in one of its superclasses, or in any implemented interfaces. In the case
|
||||
* of a nested test class, the enclosing class hierarchy will also be searched.
|
||||
* <p>A search will be performed to find the factory method in the class in
|
||||
* which the {@code @TestBean} field is declared, in one of its superclasses,
|
||||
* or in any implemented interfaces. If the {@code @TestBean} field is declared
|
||||
* in a nested test class, the enclosing class hierarchy will also be searched.
|
||||
* <p>Alternatively, a factory method in an external class can be referenced
|
||||
* via its fully-qualified method name following the syntax
|
||||
* {@code <fully-qualified class name>#<method name>} — for example,
|
||||
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
|
||||
* <p>If left unspecified, the name of the factory method will be detected
|
||||
* based either on the name of the annotated field or the name of the bean.
|
||||
* based either on the name of the {@code @TestBean} field or the {@link #name() name}
|
||||
* of the bean.
|
||||
*/
|
||||
String methodName() default "";
|
||||
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -67,7 +67,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
Method factoryMethod;
|
||||
if (!methodName.isBlank()) {
|
||||
// If the user specified an explicit method name, search for that.
|
||||
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
|
||||
factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), methodName);
|
||||
}
|
||||
else {
|
||||
// Otherwise, search for candidate factory methods whose names match either
|
||||
@@ -78,7 +78,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
if (beanName != null) {
|
||||
candidateMethodNames.add(beanName);
|
||||
}
|
||||
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
|
||||
factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), candidateMethodNames);
|
||||
}
|
||||
|
||||
return new TestBeanOverrideHandler(
|
||||
|
||||
+11
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,7 +38,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
|
||||
private final MockReset reset;
|
||||
|
||||
|
||||
protected AbstractMockitoBeanOverrideHandler(Field field, ResolvableType beanType,
|
||||
protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
|
||||
@Nullable String beanName, BeanOverrideStrategy strategy, @Nullable MockReset reset) {
|
||||
|
||||
super(field, beanType, beanName, strategy);
|
||||
@@ -56,20 +56,20 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
|
||||
|
||||
@Override
|
||||
protected void trackOverrideInstance(Object mock, SingletonBeanRegistry trackingBeanRegistry) {
|
||||
getMockitoBeans(trackingBeanRegistry).add(mock);
|
||||
getMockBeans(trackingBeanRegistry).add(mock);
|
||||
}
|
||||
|
||||
private static MockitoBeans getMockitoBeans(SingletonBeanRegistry trackingBeanRegistry) {
|
||||
String beanName = MockitoBeans.class.getName();
|
||||
MockitoBeans mockitoBeans = null;
|
||||
private static MockBeans getMockBeans(SingletonBeanRegistry trackingBeanRegistry) {
|
||||
String beanName = MockBeans.class.getName();
|
||||
MockBeans mockBeans = null;
|
||||
if (trackingBeanRegistry.containsSingleton(beanName)) {
|
||||
mockitoBeans = (MockitoBeans) trackingBeanRegistry.getSingleton(beanName);
|
||||
mockBeans = (MockBeans) trackingBeanRegistry.getSingleton(beanName);
|
||||
}
|
||||
if (mockitoBeans == null) {
|
||||
mockitoBeans = new MockitoBeans();
|
||||
trackingBeanRegistry.registerSingleton(beanName, mockitoBeans);
|
||||
if (mockBeans == null) {
|
||||
mockBeans = new MockBeans();
|
||||
trackingBeanRegistry.registerSingleton(beanName, mockBeans);
|
||||
}
|
||||
return mockitoBeans;
|
||||
return mockBeans;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/**
|
||||
* Beans created using Mockito.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
class MockBeans {
|
||||
|
||||
private final List<Object> beans = new ArrayList<>();
|
||||
|
||||
|
||||
void add(Object bean) {
|
||||
this.beans.add(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all Mockito beans configured with the supplied {@link MockReset} strategy.
|
||||
* <p>No mocks will be reset if the supplied strategy is {@link MockReset#NONE}.
|
||||
*/
|
||||
void resetAll(MockReset reset) {
|
||||
if (reset != MockReset.NONE) {
|
||||
for (Object bean : this.beans) {
|
||||
if (reset == MockReset.get(bean)) {
|
||||
Mockito.reset(bean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+62
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
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;
|
||||
@@ -29,17 +30,37 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.bean.override.BeanOverride;
|
||||
|
||||
/**
|
||||
* {@code @MockitoBean} is an annotation that can be applied to a non-static field
|
||||
* in a test class to override a bean in the test's
|
||||
* {@code @MockitoBean} is an annotation that can be used in test classes to
|
||||
* override beans in a test's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||
* using a Mockito mock.
|
||||
* using Mockito mocks.
|
||||
*
|
||||
* <p>By default, the bean to mock is inferred from the type of the annotated
|
||||
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be
|
||||
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
|
||||
* the name of the annotated field will be used as a fallback qualifier.
|
||||
* Alternatively, you can explicitly specify a bean name to mock by setting the
|
||||
* {@link #value() value} or {@link #name() name} attribute.
|
||||
* <p>{@code @MockitoBean} can be applied in the following ways.
|
||||
* <ul>
|
||||
* <li>On a non-static field in a test class or any of its superclasses.</li>
|
||||
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
|
||||
* or in any class in the type hierarchy or enclosing class hierarchy above the
|
||||
* {@code @Nested} test class.</li>
|
||||
* <li>At the type level on a test class or any superclass or implemented interface
|
||||
* in the type hierarchy above the test class.</li>
|
||||
* <li>At the type level on an enclosing class for a {@code @Nested} test class
|
||||
* or on any class or interface in the type hierarchy or enclosing class hierarchy
|
||||
* above the {@code @Nested} test class.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
|
||||
* from the type of the annotated field. If multiple candidates exist, a
|
||||
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
|
||||
* In the absence of a {@code @Qualifier} annotation, the name of the annotated
|
||||
* field will be used as a fallback qualifier. Alternatively, you can explicitly
|
||||
* specify a bean name to mock by setting the {@link #value() value} or
|
||||
* {@link #name() name} attribute.
|
||||
*
|
||||
* <p>When {@code @MockitoBean} is declared at the type level, the type of bean
|
||||
* to mock must be supplied via the {@link #types() types} attribute. If multiple
|
||||
* candidates exist, you can explicitly specify a bean name to mock by setting the
|
||||
* {@link #name() name} attribute. Note, however, that the {@code types} attribute
|
||||
* must contain a single type if an explicit bean {@code name} is configured.
|
||||
*
|
||||
* <p>A bean will be created if a corresponding bean does not exist. However, if
|
||||
* you would like for the test to fail when a corresponding bean does not exist,
|
||||
@@ -63,15 +84,29 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* (default visibility), or {@code private} depending on the needs or coding
|
||||
* practices of the project.
|
||||
*
|
||||
* <p>{@code @MockitoBean} fields and type-level {@code @MockitoBean} declarations
|
||||
* will be inherited from an enclosing test class by default. See
|
||||
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* for details.
|
||||
*
|
||||
* <p>{@code @MockitoBean} may be used as a <em>meta-annotation</em> to create custom
|
||||
* <em>composed annotations</em> — for example, to define common mock
|
||||
* configuration in a single annotation that can be reused across a test suite.
|
||||
* {@code @MockitoBean} can also be used as a <em>{@linkplain Repeatable repeatable}</em>
|
||||
* annotation at the type level — for example, to mock several beans by
|
||||
* {@link #name() name}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see org.springframework.test.context.bean.override.mockito.MockitoBeans @MockitoBeans
|
||||
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
|
||||
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Target({ElementType.FIELD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Repeatable(MockitoBeans.class)
|
||||
@BeanOverride(MockitoBeanOverrideProcessor.class)
|
||||
public @interface MockitoBean {
|
||||
|
||||
@@ -87,13 +122,27 @@ public @interface MockitoBean {
|
||||
/**
|
||||
* Name of the bean to mock.
|
||||
* <p>If left unspecified, the bean to mock is selected according to the
|
||||
* annotated field's type, taking qualifiers into account if necessary. See
|
||||
* the {@linkplain MockitoBean class-level documentation} for details.
|
||||
* configured {@link #types() types} or the annotated field's type, taking
|
||||
* qualifiers into account if necessary. See the {@linkplain MockitoBean
|
||||
* class-level documentation} for details.
|
||||
* @see #value()
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* One or more types to mock.
|
||||
* <p>Defaults to none.
|
||||
* <p>Each type specified will result in a mock being created and registered
|
||||
* with the {@code ApplicationContext}.
|
||||
* <p>Types must be omitted when the annotation is used on a field.
|
||||
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute
|
||||
* can only contain a single value.
|
||||
* @return the types to mock
|
||||
* @since 6.2.2
|
||||
*/
|
||||
Class<?>[] types() default {};
|
||||
|
||||
/**
|
||||
* Extra interfaces that should also be declared by the mock.
|
||||
* <p>Defaults to none.
|
||||
|
||||
+9
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -57,20 +57,24 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
|
||||
private final boolean serializable;
|
||||
|
||||
|
||||
MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||
MockitoBeanOverrideHandler(ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||
this(null, typeToMock, mockitoBean);
|
||||
}
|
||||
|
||||
MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
|
||||
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
|
||||
(mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
|
||||
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
|
||||
}
|
||||
|
||||
private MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, @Nullable String beanName,
|
||||
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, @Nullable Answers answers,
|
||||
private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
|
||||
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, Answers answers,
|
||||
boolean serializable) {
|
||||
|
||||
super(field, typeToMock, beanName, strategy, reset);
|
||||
Assert.notNull(typeToMock, "'typeToMock' must not be null");
|
||||
this.extraInterfaces = asClassSet(extraInterfaces);
|
||||
this.answers = (answers != null ? answers : Answers.RETURNS_DEFAULTS);
|
||||
this.answers = answers;
|
||||
this.serializable = serializable;
|
||||
}
|
||||
|
||||
|
||||
+29
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,15 +18,20 @@ package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link BeanOverrideProcessor} implementation that provides support for
|
||||
* {@link MockitoBean @MockitoBean} and {@link MockitoSpyBean @MockitoSpyBean}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see MockitoBean @MockitoBean
|
||||
* @see MockitoSpyBean @MockitoSpyBean
|
||||
@@ -35,8 +40,10 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
|
||||
@Override
|
||||
public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field) {
|
||||
if (overrideAnnotation instanceof MockitoBean mockBean) {
|
||||
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockBean);
|
||||
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
|
||||
Assert.state(mockitoBean.types().length == 0,
|
||||
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
|
||||
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
|
||||
}
|
||||
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
|
||||
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
|
||||
@@ -47,4 +54,23 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
|
||||
.formatted(field.getDeclaringClass().getName(), field.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
|
||||
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
|
||||
throw new IllegalStateException("""
|
||||
Invalid annotation passed to MockitoBeanOverrideProcessor: \
|
||||
expected @MockitoBean on test class """ + testClass.getName());
|
||||
}
|
||||
Class<?>[] types = mockitoBean.types();
|
||||
Assert.state(types.length > 0,
|
||||
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
|
||||
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
|
||||
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
|
||||
List<BeanOverrideHandler> handlers = new ArrayList<>();
|
||||
for (Class<?> type : types) {
|
||||
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-29
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,39 +16,26 @@
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Beans created using Mockito.
|
||||
* Container for {@link MockitoBean @MockitoBean} annotations which allows
|
||||
* {@code @MockitoBean} to be used as a {@linkplain java.lang.annotation.Repeatable
|
||||
* repeatable annotation} at the type level — for example, on test classes
|
||||
* or interfaces implemented by test classes.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @since 6.2.2
|
||||
*/
|
||||
class MockitoBeans {
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface MockitoBeans {
|
||||
|
||||
private final List<Object> beans = new ArrayList<>();
|
||||
|
||||
|
||||
void add(Object bean) {
|
||||
this.beans.add(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all Mockito beans configured with the supplied {@link MockReset} strategy.
|
||||
* <p>No mocks will be reset if the supplied strategy is {@link MockReset#NONE}.
|
||||
*/
|
||||
void resetAll(MockReset reset) {
|
||||
if (reset != MockReset.NONE) {
|
||||
for (Object bean : this.beans) {
|
||||
if (reset == MockReset.get(bean)) {
|
||||
Mockito.reset(bean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MockitoBean[] value();
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -116,7 +116,7 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList
|
||||
}
|
||||
}
|
||||
try {
|
||||
beanFactory.getBean(MockitoBeans.class).resetAll(reset);
|
||||
beanFactory.getBean(MockBeans.class).resetAll(reset);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
// Continue
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -56,6 +56,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* (default visibility), or {@code private} depending on the needs or coding
|
||||
* practices of the project.
|
||||
*
|
||||
* <p>{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default.
|
||||
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
|
||||
* for details.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
|
||||
+4
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -48,15 +48,9 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
|
||||
new SpringAopBypassingVerificationStartedListener();
|
||||
|
||||
|
||||
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyAnnotation) {
|
||||
this(field, typeToSpy, (StringUtils.hasText(spyAnnotation.name()) ? spyAnnotation.name() : null),
|
||||
spyAnnotation.reset());
|
||||
}
|
||||
|
||||
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, @Nullable String beanName,
|
||||
MockReset reset) {
|
||||
|
||||
super(field, typeToSpy, beanName, BeanOverrideStrategy.WRAP, reset);
|
||||
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
|
||||
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
|
||||
BeanOverrideStrategy.WRAP, spyBean.reset());
|
||||
Assert.notNull(typeToSpy, "typeToSpy must not be null");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -78,8 +78,21 @@ public class HttpHeadersAssert extends AbstractMapAssert<HttpHeadersAssert, Http
|
||||
* Verify that the actual HTTP headers do not contain any of the headers
|
||||
* with the given {@code names}.
|
||||
* @param names the names of HTTP headers that should not be present
|
||||
* @since 6.2.2
|
||||
* @see #doesNotContainKeys
|
||||
*/
|
||||
public HttpHeadersAssert doesNotContainHeaders(String... names) {
|
||||
return doesNotContainKeys(names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the actual HTTP headers do not contain any of the headers
|
||||
* with the given {@code names}.
|
||||
* @param names the names of HTTP headers that should not be present
|
||||
* @see #doesNotContainKeys
|
||||
* @deprecated in favor of {@link #doesNotContainHeaders(String...)}
|
||||
*/
|
||||
@Deprecated(since = "6.2.2", forRemoval = true)
|
||||
public HttpHeadersAssert doesNotContainsHeaders(String... names) {
|
||||
return doesNotContainKeys(names);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -395,7 +395,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
||||
|
||||
private static AnnotationConfigApplicationContext createContext(Class<?> testClass) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>(BeanOverrideHandler.forTestClass(testClass));
|
||||
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>(BeanOverrideTestUtils.findHandlers(testClass));
|
||||
new BeanOverrideContextCustomizer(handlers).customizeContext(context, mock(MergedContextConfiguration.class));
|
||||
return context;
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,14 +51,14 @@ class BeanOverrideHandlerTests {
|
||||
|
||||
@Test
|
||||
void forTestClassWithSingleField() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(SingleAnnotation.class);
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(SingleAnnotation.class);
|
||||
assertThat(handlers).singleElement().satisfies(hasBeanOverrideHandler(
|
||||
field(SingleAnnotation.class, "message"), String.class, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forTestClassWithMultipleFields() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(MultipleAnnotations.class);
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(MultipleAnnotations.class);
|
||||
assertThat(handlers).hasSize(2)
|
||||
.anySatisfy(hasBeanOverrideHandler(
|
||||
field(MultipleAnnotations.class, "message"), String.class, null))
|
||||
@@ -68,7 +68,7 @@ class BeanOverrideHandlerTests {
|
||||
|
||||
@Test
|
||||
void forTestClassWithMultipleFieldsWithIdenticalMetadata() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(MultipleAnnotationsDuplicate.class);
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(MultipleAnnotationsDuplicate.class);
|
||||
assertThat(handlers).hasSize(2)
|
||||
.anySatisfy(hasBeanOverrideHandler(
|
||||
field(MultipleAnnotationsDuplicate.class, "message1"), String.class, "messageBean"))
|
||||
@@ -81,7 +81,7 @@ class BeanOverrideHandlerTests {
|
||||
void forTestClassWithCompetingBeanOverrideAnnotationsOnSameField() {
|
||||
Field faultyField = field(MultipleAnnotationsOnSameField.class, "message");
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideHandler.forTestClass(MultipleAnnotationsOnSameField.class))
|
||||
.isThrownBy(() -> BeanOverrideTestUtils.findHandlers(MultipleAnnotationsOnSameField.class))
|
||||
.withMessageStartingWith("Multiple @BeanOverride annotations found")
|
||||
.withMessageContaining(faultyField.toString());
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class BeanOverrideHandlerTests {
|
||||
void forTestClassWithStaticBeanOverrideField() {
|
||||
Field staticField = field(StaticBeanOverrideField.class, "message");
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideHandler.forTestClass(StaticBeanOverrideField.class))
|
||||
.isThrownBy(() -> BeanOverrideTestUtils.findHandlers(StaticBeanOverrideField.class))
|
||||
.withMessage("@BeanOverride field must not be static: " + staticField);
|
||||
}
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Test utilities for Bean Overrides.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.2
|
||||
*/
|
||||
public abstract class BeanOverrideTestUtils {
|
||||
|
||||
public static List<BeanOverrideHandler> findHandlers(Class<?> testClass) {
|
||||
return BeanOverrideHandler.forTestClass(testClass);
|
||||
}
|
||||
|
||||
public static List<BeanOverrideHandler> findAllHandlers(Class<?> testClass) {
|
||||
return BeanOverrideHandler.findAllHandlers(testClass);
|
||||
}
|
||||
|
||||
}
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.convention;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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.bean.override.convention.TestBeanForInheritanceIntegrationTests.AbstractTestBeanIntegrationTestCase.FakePojo;
|
||||
import org.springframework.test.context.bean.override.convention.TestBeanForInheritanceIntegrationTests.AbstractTestBeanIntegrationTestCase.Pojo;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link TestBean} that use inheritance.
|
||||
*
|
||||
* <p>Tests inheritance within a class hierarchy as well as "inheritance" within
|
||||
* an enclosing class hierarchy.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
public class TestBeanForInheritanceIntegrationTests {
|
||||
|
||||
static Pojo enclosingClassBeanOverride() {
|
||||
return new FakePojo("in enclosing test class");
|
||||
}
|
||||
|
||||
@SpringJUnitConfig
|
||||
public abstract static class AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@TestBean(name = "someBean")
|
||||
Pojo someBean;
|
||||
|
||||
@TestBean(name = "otherBean")
|
||||
Pojo otherBean;
|
||||
|
||||
@TestBean(name = "thirdBean")
|
||||
Pojo anotherBean;
|
||||
|
||||
static Pojo otherBean() {
|
||||
return new FakePojo("otherBean in superclass");
|
||||
}
|
||||
|
||||
static Pojo thirdBean() {
|
||||
return new FakePojo("third in superclass");
|
||||
}
|
||||
|
||||
static Pojo commonBeanOverride() {
|
||||
return new FakePojo("in superclass");
|
||||
}
|
||||
|
||||
public interface Pojo {
|
||||
|
||||
default String getValue() {
|
||||
return "Prod";
|
||||
}
|
||||
}
|
||||
|
||||
static class ProdPojo implements Pojo { }
|
||||
|
||||
static class FakePojo implements Pojo {
|
||||
final String value;
|
||||
|
||||
protected FakePojo(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
Pojo someBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo otherBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo thirdBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo pojo() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
@Bean
|
||||
Pojo pojo2() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Nested, concrete inherited tests with correct @TestBean setup")
|
||||
class NestedConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
|
||||
|
||||
@Autowired
|
||||
ApplicationContext ctx;
|
||||
|
||||
@TestBean(name = "pojo", methodName = "commonBeanOverride")
|
||||
Pojo pojo;
|
||||
|
||||
@TestBean(name = "pojo2", methodName = "enclosingClassBeanOverride")
|
||||
Pojo pojo2;
|
||||
|
||||
static Pojo someBean() {
|
||||
return new FakePojo("someBeanOverride");
|
||||
}
|
||||
|
||||
// Hides otherBean() defined in AbstractTestBeanIntegrationTestCase.
|
||||
static Pojo otherBean() {
|
||||
return new FakePojo("otherBean in subclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSubtypeWithFactoryMethodInSupertype() {
|
||||
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("in superclass");
|
||||
assertThat(this.pojo.getValue()).as("injection point").isEqualTo("in superclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSupertypeWithFactoryMethodInSubtype() {
|
||||
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
|
||||
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSupertypeWithPrioritizedFactoryMethodInSubtype() {
|
||||
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("otherBean in subclass");
|
||||
assertThat(super.otherBean.getValue()).as("injection point").isEqualTo("otherBean in subclass");
|
||||
}
|
||||
@Test
|
||||
void fieldInNestedClassWithFactoryMethodInEnclosingClass() {
|
||||
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
|
||||
assertThat(this.pojo2.getValue()).as("injection point").isEqualTo("in enclosing test class");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.convention;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link TestBean} that use inheritance.
|
||||
*
|
||||
* <p>Tests inheritance within a class hierarchy as well as "inheritance" within
|
||||
* an enclosing class hierarchy.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
public class TestBeanInheritanceIntegrationTests {
|
||||
|
||||
@TestBean
|
||||
Pojo puzzleBean;
|
||||
|
||||
static Pojo puzzleBean() {
|
||||
return new FakePojo("puzzle in enclosing class");
|
||||
}
|
||||
|
||||
static Pojo enclosingClassFactoryMethod() {
|
||||
return new FakePojo("in enclosing test class");
|
||||
}
|
||||
|
||||
abstract static class AbstractTestCase {
|
||||
|
||||
@TestBean("otherBean")
|
||||
Pojo otherBean;
|
||||
|
||||
@TestBean
|
||||
Pojo anotherBean;
|
||||
|
||||
@TestBean
|
||||
Pojo enigmaBean;
|
||||
|
||||
static Pojo otherBean() {
|
||||
return new FakePojo("other in superclass");
|
||||
}
|
||||
|
||||
static Pojo anotherBean() {
|
||||
return new FakePojo("another in superclass");
|
||||
}
|
||||
|
||||
static Pojo enigmaBean() {
|
||||
return new FakePojo("enigma in superclass");
|
||||
}
|
||||
|
||||
static Pojo commonBean() {
|
||||
return new FakePojo("common in superclass");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Nested, concrete inherited tests with correct @TestBean setup")
|
||||
class NestedTests extends AbstractTestCase {
|
||||
|
||||
@Autowired
|
||||
ApplicationContext ctx;
|
||||
|
||||
@TestBean(methodName = "commonBean")
|
||||
Pojo pojo;
|
||||
|
||||
@TestBean(name = "pojo2", methodName = "enclosingClassFactoryMethod")
|
||||
Pojo pojo2;
|
||||
|
||||
@TestBean
|
||||
Pojo enigmaBean;
|
||||
|
||||
@TestBean
|
||||
Pojo puzzleBean;
|
||||
|
||||
|
||||
// "Overrides" puzzleBean() defined in TestBeanInheritanceIntegrationTests.
|
||||
static Pojo puzzleBean() {
|
||||
return new FakePojo("puzzle in nested class");
|
||||
}
|
||||
|
||||
// "Overrides" enigmaBean() defined in AbstractTestCase.
|
||||
static Pojo enigmaBean() {
|
||||
return new FakePojo("enigma in subclass");
|
||||
}
|
||||
|
||||
static Pojo otherBean() {
|
||||
return new FakePojo("other in subclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSuperclassWithFactoryMethodInSuperclass() {
|
||||
assertThat(ctx.getBean("anotherBean")).as("applicationContext").hasToString("another in superclass");
|
||||
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("another in superclass");
|
||||
}
|
||||
|
||||
@Test // gh-34204
|
||||
void fieldInSuperclassWithFactoryMethodInSuperclassAndInSubclass() {
|
||||
// We do not expect "other in subclass", because the @TestBean declaration in
|
||||
// AbstractTestCase cannot "see" the otherBean() factory method in the subclass.
|
||||
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in superclass");
|
||||
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in superclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInSubclassWithFactoryMethodInSuperclass() {
|
||||
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("common in superclass");
|
||||
assertThat(this.pojo.value()).as("injection point").isEqualTo("common in superclass");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldInNestedClassWithFactoryMethodInEnclosingClass() {
|
||||
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
|
||||
assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class");
|
||||
}
|
||||
|
||||
@Test // gh-34194, gh-34204
|
||||
void testBeanInSubclassOverridesTestBeanInSuperclass() {
|
||||
assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass");
|
||||
assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass");
|
||||
}
|
||||
|
||||
@Test // gh-34194, gh-34204
|
||||
void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
|
||||
assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class");
|
||||
assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("puzzle in nested class");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
Pojo otherBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Pojo anotherBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Pojo enigmaBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Pojo puzzleBean() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Pojo pojo() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Pojo pojo2() {
|
||||
return new ProdPojo();
|
||||
}
|
||||
}
|
||||
|
||||
interface Pojo {
|
||||
|
||||
default String value() {
|
||||
return "prod";
|
||||
}
|
||||
}
|
||||
|
||||
static class ProdPojo implements Pojo {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value();
|
||||
}
|
||||
}
|
||||
|
||||
record FakePojo(String value) implements Pojo {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+7
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -42,21 +43,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
class TestBeanOverrideHandlerTests {
|
||||
|
||||
@Test
|
||||
void setsBeanNameToNullIfAnnotationNameIsNull() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(SampleOneOverride.class);
|
||||
void beanNameIsSetToNullIfAnnotationNameIsEmpty() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(SampleOneOverride.class);
|
||||
assertThat(handlers).singleElement().extracting(BeanOverrideHandler::getBeanName).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setsBeanNameToAnnotationName() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(SampleOneOverrideWithName.class);
|
||||
void beanNameIsSetToAnnotationName() {
|
||||
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(SampleOneOverrideWithName.class);
|
||||
assertThat(handlers).singleElement().extracting(BeanOverrideHandler::getBeanName).isEqualTo("anotherBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWithMissingMethod() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideHandler.forTestClass(SampleMissingMethod.class))
|
||||
.isThrownBy(() -> BeanOverrideTestUtils.findHandlers(SampleMissingMethod.class))
|
||||
.withMessage("No static method found named message() in %s with return type %s",
|
||||
SampleMissingMethod.class.getName(), String.class.getName());
|
||||
}
|
||||
|
||||
+22
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,9 +26,10 @@ import org.springframework.test.context.bean.override.BeanOverrideContextCustomi
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link TestBean}.
|
||||
* Tests for {@link TestBean @TestBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class TestBeanTests {
|
||||
|
||||
@@ -64,8 +65,8 @@ public class TestBeanTests {
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("""
|
||||
Unable to override bean: there are no beans of \
|
||||
type %s (as required by field '%s.example').""".formatted(
|
||||
String.class.getName(), FailureByTypeLookup.class.getSimpleName()));
|
||||
type %s (as required by field '%s.example').""",
|
||||
String.class.getName(), FailureByTypeLookup.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,8 +88,8 @@ public class TestBeanTests {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
|
||||
FailureMissingDefaultOverrideMethod.class, context))
|
||||
.withMessage("No static method found named example() or beanToOverride() in %s with return type %s"
|
||||
.formatted(FailureMissingDefaultOverrideMethod.class.getName(), String.class.getName()));
|
||||
.withMessage("No static method found named example() or beanToOverride() in %s with return type %s",
|
||||
FailureMissingDefaultOverrideMethod.class.getName(), String.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,8 +98,8 @@ public class TestBeanTests {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
|
||||
FailureMissingExplicitOverrideMethod.class, context))
|
||||
.withMessage("No static method found named createExample() in %s with return type %s"
|
||||
.formatted(FailureMissingExplicitOverrideMethod.class.getName(), String.class.getName()));
|
||||
.withMessage("No static method found named createExample() in %s with return type %s",
|
||||
FailureMissingExplicitOverrideMethod.class.getName(), String.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,19 +109,19 @@ public class TestBeanTests {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
|
||||
FailureOverrideInParentWithoutFactoryMethod.class, context))
|
||||
.withMessage("No static method found named beanToOverride() in %s with return type %s"
|
||||
.formatted(FailureOverrideInParentWithoutFactoryMethod.class.getName(), String.class.getName()));
|
||||
.withMessage("No static method found named beanToOverride() in %s with return type %s",
|
||||
AbstractByNameLookup.class.getName(), String.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextCustomizerCannotBeCreatedWitCompetingOverrideMethods() {
|
||||
void contextCustomizerCannotBeCreatedWithCompetingOverrideMethods() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.registerBean("bean", String.class, () -> "example");
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
|
||||
FailureCompetingOverrideMethods.class, context))
|
||||
.withMessage("Found 2 competing static methods named example() or beanToOverride() in %s with return type %s"
|
||||
.formatted(FailureCompetingOverrideMethods.class.getName(), String.class.getName()));
|
||||
.withMessage("Found 2 competing static methods named example() or beanToOverride() in %s with return type %s",
|
||||
FailureCompetingOverrideMethods.class.getName(), String.class.getName());
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +150,7 @@ public class TestBeanTests {
|
||||
@TestBean(name = "beanToOverride")
|
||||
private String example;
|
||||
|
||||
// Expected static String example() { ... }
|
||||
// or static String beanToOverride() { ... }
|
||||
|
||||
// No example() or beanToOverride() method
|
||||
}
|
||||
|
||||
static class FailureMissingExplicitOverrideMethod {
|
||||
@@ -159,28 +158,22 @@ public class TestBeanTests {
|
||||
@TestBean(methodName = "createExample")
|
||||
private String example;
|
||||
|
||||
// Expected static String createExample() { ... }
|
||||
|
||||
// NO createExample() method
|
||||
}
|
||||
|
||||
abstract static class AbstractByNameLookup {
|
||||
|
||||
@TestBean(methodName = "beanToOverride")
|
||||
protected String beanToOverride;
|
||||
@TestBean
|
||||
String beanToOverride;
|
||||
|
||||
// No beanToOverride() method
|
||||
}
|
||||
|
||||
static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
|
||||
|
||||
// No beanToOverride() method
|
||||
|
||||
}
|
||||
|
||||
abstract static class AbstractCompetingMethods {
|
||||
|
||||
@TestBean(name = "beanToOverride")
|
||||
protected String example;
|
||||
|
||||
static String example() {
|
||||
throw new IllegalStateException("Should not be called");
|
||||
}
|
||||
@@ -188,10 +181,12 @@ public class TestBeanTests {
|
||||
|
||||
static class FailureCompetingOverrideMethods extends AbstractCompetingMethods {
|
||||
|
||||
@TestBean(name = "beanToOverride")
|
||||
String example;
|
||||
|
||||
static String beanToOverride() {
|
||||
throw new IllegalStateException("Should not be called");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+125
-26
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,6 +26,7 @@ import org.mockito.Answers;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -34,104 +35,179 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests for {@link MockitoBeanOverrideHandler}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
class MockitoBeanOverrideHandlerTests {
|
||||
|
||||
@Test
|
||||
void forTestClassSetsNameToNullIfAnnotationNameIsNull() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneMock.class);
|
||||
void beanNameIsSetToNullIfAnnotationNameIsEmpty() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneMock.class);
|
||||
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void forTestClassSetsNameToAnnotationName() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneMockWithName.class);
|
||||
void beanNameIsSetToAnnotationName() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneMockWithName.class);
|
||||
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isEqualTo("anotherService"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEqualToWithSameInstance() {
|
||||
MockitoBeanOverrideHandler handler = createBeanOverrideHandler(sampleField("service"));
|
||||
void isEqualToWithSameInstanceFromField() {
|
||||
MockitoBeanOverrideHandler handler = createHandler(sampleField("service"));
|
||||
assertThat(handler).isEqualTo(handler);
|
||||
assertThat(handler).hasSameHashCodeAs(handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEqualToWithSameMetadata() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service"));
|
||||
void isEqualToWithSameMetadataFromField() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service"));
|
||||
assertThat(handler1).isEqualTo(handler2);
|
||||
assertThat(handler1).hasSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test // gh-33925
|
||||
void isEqualToWithSameInstanceFromClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
|
||||
assertThat(handler1).isEqualTo(handler1);
|
||||
assertThat(handler1).hasSameHashCodeAs(handler1);
|
||||
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class);
|
||||
assertThat(handler2).isEqualTo(handler2);
|
||||
assertThat(handler2).hasSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test // gh-33925
|
||||
void isEqualToWithSameByNameLookupMetadataFromClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName2.class);
|
||||
assertThat(handler1).isEqualTo(handler2);
|
||||
assertThat(handler2).isEqualTo(handler1);
|
||||
assertThat(handler1).hasSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test // gh-33925
|
||||
void isNotEqualToWithDifferentByNameLookupMetadataFromClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName3.class);
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
assertThat(handler2).isNotEqualTo(handler1);
|
||||
assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test // gh-33925
|
||||
void isEqualToWithSameByTypeLookupMetadataFromClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class);
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType2.class);
|
||||
assertThat(handler1).isEqualTo(handler2);
|
||||
assertThat(handler2).isEqualTo(handler1);
|
||||
assertThat(handler1).hasSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test // gh-33925
|
||||
void isNotEqualToWithDifferentByTypeLookupMetadataFromClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class);
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType3.class);
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
assertThat(handler2).isNotEqualTo(handler1);
|
||||
assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test // gh-33925
|
||||
void isEqualToWithSameByNameLookupMetadataFromFieldAndClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service3"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName1.class);
|
||||
assertThat(handler1).isEqualTo(handler2);
|
||||
assertThat(handler2).isEqualTo(handler1);
|
||||
assertThat(handler1).hasSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the "field name as fallback qualifier" is not available for an annotated class,
|
||||
* what would seem to be "equivalent" handlers are actually not considered "equal" when
|
||||
* the the lookup is "by type".
|
||||
*/
|
||||
@Test // gh-33925
|
||||
void isNotEqualToWithSameByTypeLookupMetadataFromFieldAndClassLevel() {
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class);
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
assertThat(handler2).isNotEqualTo(handler1);
|
||||
assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotEqualEqualToByTypeLookupWithSameMetadataButDifferentField() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service2"));
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service2"));
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEqualEqualToByNameLookupWithSameMetadataButDifferentField() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service3"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service4"));
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service3"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service4"));
|
||||
assertThat(handler1).isEqualTo(handler2);
|
||||
assertThat(handler1).hasSameHashCodeAs(handler2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotEqualToWithSameMetadataButDifferentBeanName() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service3"));
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service3"));
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotEqualToWithSameMetadataButDifferentExtraInterfaces() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service5"));
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service5"));
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotEqualToWithSameMetadataButDifferentAnswers() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service6"));
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service6"));
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotEqualToWithSameMetadataButDifferentSerializableFlag() {
|
||||
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service7"));
|
||||
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
|
||||
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service7"));
|
||||
assertThat(handler1).isNotEqualTo(handler2);
|
||||
}
|
||||
|
||||
|
||||
private Field sampleField(String fieldName) {
|
||||
private static Field sampleField(String fieldName) {
|
||||
Field field = ReflectionUtils.findField(Sample.class, fieldName);
|
||||
assertThat(field).isNotNull();
|
||||
return field;
|
||||
}
|
||||
|
||||
private MockitoBeanOverrideHandler createBeanOverrideHandler(Field field) {
|
||||
private static MockitoBeanOverrideHandler createHandler(Field field) {
|
||||
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(field, MockitoBean.class);
|
||||
return new MockitoBeanOverrideHandler(field, ResolvableType.forClass(field.getType()), annotation);
|
||||
}
|
||||
|
||||
private MockitoBeanOverrideHandler createHandler(Class<?> clazz) {
|
||||
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(clazz, MockitoBean.class);
|
||||
return new MockitoBeanOverrideHandler(null, ResolvableType.forClass(annotation.types()[0]), annotation);
|
||||
}
|
||||
|
||||
|
||||
static class SampleOneMock {
|
||||
|
||||
@MockitoBean
|
||||
String service;
|
||||
|
||||
}
|
||||
|
||||
static class SampleOneMockWithName {
|
||||
|
||||
@MockitoBean("anotherService")
|
||||
String service;
|
||||
|
||||
}
|
||||
|
||||
static class Sample {
|
||||
@@ -156,7 +232,30 @@ class MockitoBeanOverrideHandlerTests {
|
||||
|
||||
@MockitoBean(serializable = true)
|
||||
private String service7;
|
||||
}
|
||||
|
||||
@MockitoBean(name = "beanToMock", types = String.class)
|
||||
static class ClassLevelStringMockByName1 {
|
||||
}
|
||||
|
||||
@MockitoBean(name = "beanToMock", types = String.class)
|
||||
static class ClassLevelStringMockByName2 {
|
||||
}
|
||||
|
||||
@MockitoBean(name = "otherBeanToMock", types = String.class)
|
||||
static class ClassLevelStringMockByName3 {
|
||||
}
|
||||
|
||||
@MockitoBean(types = String.class)
|
||||
static class ClassLevelStringMockByType1 {
|
||||
}
|
||||
|
||||
@MockitoBean(types = String.class)
|
||||
static class ClassLevelStringMockByType2 {
|
||||
}
|
||||
|
||||
@MockitoBean(types = Integer.class)
|
||||
static class ClassLevelStringMockByType3 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+152
-34
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,62 +18,180 @@ package org.springframework.test.context.bean.override.mockito;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link MockitoBeanOverrideProcessorTests}
|
||||
* Tests for {@link MockitoBeanOverrideProcessor}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
*/
|
||||
public class MockitoBeanOverrideProcessorTests {
|
||||
class MockitoBeanOverrideProcessorTests {
|
||||
|
||||
private final MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
|
||||
|
||||
@Test
|
||||
void mockAnnotationCreatesMockitoBeanOverrideHandler() throws NoSuchFieldException {
|
||||
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
BeanOverrideHandler object = this.processor.createHandler(annotation, clazz, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
|
||||
@Nested
|
||||
class CreateHandlerTests {
|
||||
|
||||
private final Field field = ReflectionUtils.findField(TestCase.class, "number");
|
||||
|
||||
|
||||
@Test
|
||||
void mockAnnotationCreatesMockitoBeanOverrideHandler() {
|
||||
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
|
||||
BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() {
|
||||
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
|
||||
BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherAnnotationThrows() {
|
||||
Annotation annotation = field.getAnnotation(Nullable.class);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
|
||||
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
|
||||
"@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
|
||||
field.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void typesNotSupportedAtFieldLevel() {
|
||||
Field field = ReflectionUtils.findField(TestCase.class, "typesNotSupported");
|
||||
MockitoBean annotation = field.getAnnotation(MockitoBean.class);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
|
||||
.withMessage("The @MockitoBean 'types' attribute must be omitted when declared on a field");
|
||||
}
|
||||
|
||||
|
||||
static class TestCase {
|
||||
|
||||
@Nullable
|
||||
@MockitoBean
|
||||
@MockitoSpyBean
|
||||
Integer number;
|
||||
|
||||
@MockitoBean(types = Integer.class)
|
||||
String typesNotSupported;
|
||||
}
|
||||
|
||||
@MockitoBean(name = "bogus", types = Integer.class)
|
||||
static class NameNotSupportedTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() throws NoSuchFieldException {
|
||||
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
BeanOverrideHandler object = this.processor.createHandler(annotation, clazz, field);
|
||||
@Nested
|
||||
class CreateHandlersTests {
|
||||
|
||||
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
|
||||
}
|
||||
@Test
|
||||
void missingTypes() {
|
||||
Class<?> testClass = MissingTypesTestCase.class;
|
||||
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
|
||||
|
||||
@Test
|
||||
void otherAnnotationThrows() throws NoSuchFieldException {
|
||||
Class<?> clazz = MockitoConf.class;
|
||||
Field field = clazz.getField("a");
|
||||
Annotation annotation = field.getAnnotation(Nullable.class);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
|
||||
.withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nameNotSupportedWithMultipleTypes() {
|
||||
Class<?> testClass = NameNotSupportedWithMultipleTypesTestCase.class;
|
||||
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
|
||||
.withMessage("The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleMockByType() {
|
||||
Class<?> testClass = SingleMockByTypeTestCase.class;
|
||||
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
|
||||
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
|
||||
|
||||
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
|
||||
assertThat(handler.getField()).isNull();
|
||||
assertThat(handler.getBeanName()).isNull();
|
||||
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleMockByName() {
|
||||
Class<?> testClass = SingleMockByNameTestCase.class;
|
||||
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
|
||||
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
|
||||
|
||||
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
|
||||
assertThat(handler.getField()).isNull();
|
||||
assertThat(handler.getBeanName()).isEqualTo("enigma");
|
||||
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleMocks() {
|
||||
Class<?> testClass = MultipleMocksTestCase.class;
|
||||
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
|
||||
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
|
||||
|
||||
assertThat(handlers).satisfiesExactly(
|
||||
handler1 -> {
|
||||
assertThat(handler1.getField()).isNull();
|
||||
assertThat(handler1.getBeanName()).isNull();
|
||||
assertThat(handler1.getBeanType().resolve()).isEqualTo(Integer.class);
|
||||
},
|
||||
handler2 -> {
|
||||
assertThat(handler2.getField()).isNull();
|
||||
assertThat(handler2.getBeanName()).isNull();
|
||||
assertThat(handler2.getBeanType().resolve()).isEqualTo(Float.class);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.processor.createHandler(annotation, clazz, field))
|
||||
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
|
||||
"@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
|
||||
field.getName());
|
||||
}
|
||||
|
||||
static class MockitoConf {
|
||||
@Nullable
|
||||
@MockitoBean
|
||||
@MockitoSpyBean
|
||||
public ExampleService a;
|
||||
static class MissingTypesTestCase {
|
||||
}
|
||||
|
||||
@MockitoBean(name = "bogus", types = { Integer.class, Float.class })
|
||||
static class NameNotSupportedWithMultipleTypesTestCase {
|
||||
}
|
||||
|
||||
@MockitoBean(types = Integer.class)
|
||||
static class SingleMockByTypeTestCase {
|
||||
}
|
||||
|
||||
@MockitoBean(name = "enigma", types = Integer.class)
|
||||
static class SingleMockByNameTestCase {
|
||||
}
|
||||
|
||||
@MockitoBean(types = { Integer.class, Float.class })
|
||||
static class MultipleMocksTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -36,14 +37,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class MockitoSpyBeanOverrideHandlerTests {
|
||||
|
||||
@Test
|
||||
void forTestClassSetsNameToNullIfAnnotationNameIsNull() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneSpy.class);
|
||||
void beanNameIsSetToNullIfAnnotationNameIsEmpty() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneSpy.class);
|
||||
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void forTestClassSetsNameToAnnotationName() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneSpyWithName.class);
|
||||
void beanNameIsSetToAnnotationName() {
|
||||
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneSpyWithName.class);
|
||||
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isEqualTo("anotherService"));
|
||||
}
|
||||
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito.mockbeans;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBeans;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link MockitoBeans @MockitoBeans} and
|
||||
* {@link MockitoBean @MockitoBean} declared "by name" at the class level as a
|
||||
* repeatable annotation.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.2
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/33925">gh-33925</a>
|
||||
* @see MockitoBeansByTypeIntegrationTests
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@MockitoBean(name = "s1", types = ExampleService.class)
|
||||
@MockitoBean(name = "s2", types = ExampleService.class)
|
||||
class MockitoBeansByNameIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
ExampleService s1;
|
||||
|
||||
@Autowired
|
||||
ExampleService s2;
|
||||
|
||||
@MockitoBean(name = "s3")
|
||||
ExampleService service3;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("s4")
|
||||
ExampleService service4;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void configureMocks() {
|
||||
given(s1.greeting()).willReturn("mock 1");
|
||||
given(s2.greeting()).willReturn("mock 2");
|
||||
given(service3.greeting()).willReturn("mock 3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkMocksAndStandardBean() {
|
||||
assertThat(s1.greeting()).isEqualTo("mock 1");
|
||||
assertThat(s2.greeting()).isEqualTo("mock 2");
|
||||
assertThat(service3.greeting()).isEqualTo("mock 3");
|
||||
assertThat(service4.greeting()).isEqualTo("prod 4");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
ExampleService s1() {
|
||||
return () -> "prod 1";
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExampleService s2() {
|
||||
return () -> "prod 2";
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExampleService s3() {
|
||||
return () -> "prod 3";
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExampleService s4() {
|
||||
return () -> "prod 4";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito.mockbeans;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBeans;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link MockitoBeans @MockitoBeans} and
|
||||
* {@link MockitoBean @MockitoBean} declared "by type" at the class level, as a
|
||||
* repeatable annotation, and via a custom composed annotation.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.2
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/33925">gh-33925</a>
|
||||
* @see MockitoBeansByNameIntegrationTests
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@MockitoBean(types = {Service04.class, Service05.class})
|
||||
@SharedMocks // Intentionally declared between local @MockitoBean declarations
|
||||
@MockitoBean(types = Service06.class)
|
||||
class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
|
||||
|
||||
@Autowired
|
||||
Service01 service01;
|
||||
|
||||
@Autowired
|
||||
Service02 service02;
|
||||
|
||||
@Autowired
|
||||
Service03 service03;
|
||||
|
||||
@Autowired
|
||||
Service04 service04;
|
||||
|
||||
@Autowired
|
||||
Service05 service05;
|
||||
|
||||
@Autowired
|
||||
Service06 service06;
|
||||
|
||||
@MockitoBean
|
||||
Service07 service07;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void configureMocks() {
|
||||
given(service01.greeting()).willReturn("mock 01");
|
||||
given(service02.greeting()).willReturn("mock 02");
|
||||
given(service03.greeting()).willReturn("mock 03");
|
||||
given(service04.greeting()).willReturn("mock 04");
|
||||
given(service05.greeting()).willReturn("mock 05");
|
||||
given(service06.greeting()).willReturn("mock 06");
|
||||
given(service07.greeting()).willReturn("mock 07");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkMocks() {
|
||||
assertThat(service01.greeting()).isEqualTo("mock 01");
|
||||
assertThat(service02.greeting()).isEqualTo("mock 02");
|
||||
assertThat(service03.greeting()).isEqualTo("mock 03");
|
||||
assertThat(service04.greeting()).isEqualTo("mock 04");
|
||||
assertThat(service05.greeting()).isEqualTo("mock 05");
|
||||
assertThat(service06.greeting()).isEqualTo("mock 06");
|
||||
assertThat(service07.greeting()).isEqualTo("mock 07");
|
||||
}
|
||||
|
||||
|
||||
@MockitoBean(types = Service09.class)
|
||||
static class BaseTestCase implements TestInterface08 {
|
||||
|
||||
@Autowired
|
||||
Service08 service08;
|
||||
|
||||
@Autowired
|
||||
Service09 service09;
|
||||
|
||||
@MockitoBean
|
||||
Service10 service10;
|
||||
}
|
||||
|
||||
@Nested
|
||||
@MockitoBean(types = Service12.class)
|
||||
class NestedTests extends BaseTestCase implements TestInterface11 {
|
||||
|
||||
@Autowired
|
||||
Service11 service11;
|
||||
|
||||
@Autowired
|
||||
Service12 service12;
|
||||
|
||||
@MockitoBean
|
||||
Service13 service13;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void configureMocks() {
|
||||
given(service08.greeting()).willReturn("mock 08");
|
||||
given(service09.greeting()).willReturn("mock 09");
|
||||
given(service10.greeting()).willReturn("mock 10");
|
||||
given(service11.greeting()).willReturn("mock 11");
|
||||
given(service12.greeting()).willReturn("mock 12");
|
||||
given(service13.greeting()).willReturn("mock 13");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkMocks() {
|
||||
assertThat(service01.greeting()).isEqualTo("mock 01");
|
||||
assertThat(service02.greeting()).isEqualTo("mock 02");
|
||||
assertThat(service03.greeting()).isEqualTo("mock 03");
|
||||
assertThat(service04.greeting()).isEqualTo("mock 04");
|
||||
assertThat(service05.greeting()).isEqualTo("mock 05");
|
||||
assertThat(service06.greeting()).isEqualTo("mock 06");
|
||||
assertThat(service07.greeting()).isEqualTo("mock 07");
|
||||
assertThat(service08.greeting()).isEqualTo("mock 08");
|
||||
assertThat(service09.greeting()).isEqualTo("mock 09");
|
||||
assertThat(service10.greeting()).isEqualTo("mock 10");
|
||||
assertThat(service11.greeting()).isEqualTo("mock 11");
|
||||
assertThat(service12.greeting()).isEqualTo("mock 12");
|
||||
assertThat(service13.greeting()).isEqualTo("mock 13");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito.mockbeans;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBeans;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MockitoBeans @MockitoBeans}: {@link MockitoBean @MockitoBean}
|
||||
* declared at the class level, as a repeatable annotation, and via a custom composed
|
||||
* annotation.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.2
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/33925">gh-33925</a>
|
||||
*/
|
||||
class MockitoBeansTests {
|
||||
|
||||
@Test
|
||||
void registrationOrderForTopLevelClass() {
|
||||
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.class);
|
||||
assertThat(mockedServices).containsExactly(
|
||||
Service01.class, Service02.class, Service03.class, Service04.class,
|
||||
Service05.class, Service06.class, Service07.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void registrationOrderForNestedClass() {
|
||||
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.NestedTests.class);
|
||||
assertThat(mockedServices).containsExactly(
|
||||
Service01.class, Service02.class, Service03.class, Service04.class,
|
||||
Service05.class, Service06.class, Service07.class, Service08.class,
|
||||
Service09.class, Service10.class, Service11.class, Service12.class,
|
||||
Service13.class);
|
||||
}
|
||||
|
||||
|
||||
private static Stream<Class<?>> getRegisteredMockTypes(Class<?> testClass) {
|
||||
return BeanOverrideTestUtils.findAllHandlers(testClass)
|
||||
.stream()
|
||||
.map(BeanOverrideHandler::getBeanType)
|
||||
.map(ResolvableType::getRawClass);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito.mockbeans;
|
||||
|
||||
interface Service {
|
||||
String greeting();
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito.mockbeans;
|
||||
|
||||
interface Service01 extends Service {
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.bean.override.mockito.mockbeans;
|
||||
|
||||
interface Service02 extends Service {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user