spring-boot-testjars
spring-boot-testjars copied to clipboard
= spring-boot-testjars :TESTJARS_VERSION: 0.0.1
https://docs.spring.io/spring-boot/docs/3.2.1/reference/html/features.html#features.testcontainers[Spring Boot + Testcontainers support] for Spring Boot applications, but without the need for a Docker container.
This project is intended to add support for running external Spring Boot applications during development and testing by building on Spring Boot's existing support. It is composed of two main features:
- Adding the ability to easily <<start-external,start an External Spring Boot application>>
- <<dynamicproperty,Extending Spring Boot's support for
DynamicPropertyRegistry
>>
NOTE: This project is an experimental project and may make breaking changes including, but not limited to, the name of the project.
== Videos
- https://spring.io/blog/2024/02/08/spring-tips-spring-boot-testjars[Spring Tips]
== Motivation
Why not just create a Docker image of the Spring Boot application and use Testcontainers?
- Lighter weight than spinning up a Docker image
- When you are consuming dependencies, you don't always have a docker image available. Sure you can create one, but why add additional overhead?
== Maven / Gradle
You can add Spring Boot Testjars to your project by adding them to your Gradle or Maven build.
.build.gradle [source,groovy,subs=attributes+]
testImplementation("org.springframework.experimental.boot:spring-boot-testjars:$TESTJARS_VERSION")
.pom.xml [source,xml,subs=attributes+]
Releases are published to Maven Central. For any other release type, refer to the https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts#spring-repositories[Spring Repositories].
[[starting-external]] == Starting an External Spring Boot Application
This project allows users to easily start an external Spring Boot application by creating it as a Bean. For example, the code below will start (on a arbitrary available port) and stop an external Spring Boot application as a part of the lifecycle of the Spring container:
[source,java]
@Bean static CommonsExecWebServerFactoryBean messagesApiServer() { return CommonsExecWebServerFactoryBean.builder() .classpath((cp) -> cp .files("build/libs/messages-0.0.1-SNAPSHOT.jar") ); }
The CommonsExecWebServerFactoryBean
creates a CommonsExecWebServer
and the property CommonsExecWebServer.getPort()
returns the port that the application starts on.
=== MavenClasspathEntry
User's can also resolve Maven dependencies from Maven Central. The following will add spring-boot-starter-authorization-server and it's transitive dependencies to the classpath.
[source,java]
@Bean @OAuth2ClientProviderIssuerUri static CommonsExecWebServerFactoryBean authorizationServer() { // @formatter:off return CommonsExecWebServerFactoryBean.builder() // ... .classpath((classpath) -> classpath // Add spring-boot-starter-authorization-server & transitive dependencies .entries(springBootStarter("oauth2-authorization-server")) ); // @formatter:on }
You can also use the MavenClasspathEntry
constructor directly or additional helper methods to add dependencies other than Spring Boot starters to the classpath.
To use this feature, use the https://docs.gradle.org/current/userguide/feature_variants.html[feature variant] named maven
by adding it to your Gradle or Maven build.
.build.gradle [source,groovy,subs=attributes+]
testImplementation ('org.springframework.experimental.boot:spring-boot-testjars:{TESTJARS_VERSION}') { capabilities { requireCapability("org.springframework.experimental.boot:spring-boot-testjars-maven") } }
.pom.xml [source,xml,subs=attributes+]
==== Custom Maven Repositories
By default, only Maven Central is searched. You can customize the repositories that are searched by injecting the repositories like the example below:
[source,java]
List<RemoteRepository> repositories = new ArrayList<>(); repositories.add(new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build()); repositories.add(new RemoteRepository.Builder("spring-milestone", "default", "https://repo.spring.io/milestone/").build()); MavenClasspathEntry classpathEntry = new MavenClasspathEntry("org.springframework:spring-core:6.1.0-RC1", repositories);
=== Default Java Main
In some cases you need to provide a Java main class without any additional configuration. If that is the case, you can do it with the following:
[source,java]
@Bean @OAuth2ClientProviderIssuerUri static CommonsExecWebServerFactoryBean authorizationServer() { // @formatter:off return CommonsExecWebServerFactoryBean.builder() // ... // Add a class annotated with SpringBootApplication and a main method to the classpath and use it as the main class .defaultSpringBootApplicationMain(); // @formatter:on }
=== Default application.yml / application.properties
If present, CommonsExecWebServerFactoryBean
will add the resources webjars/$beanName/application.yml
or webjars/$beanName/application.properties
exists, then it is automatically added to the classpath as application.yml
and application.properties
respectively.
[[dynamicproperty]] == @DynamicProperty
This is an extension to Spring Boot's existing https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testcontainers.at-development-time.dynamic-properties[DynamicPropertyRegistry
].
It allows annotating arbitrary Spring Bean definitions and adding a property that references properties on that Bean.
For example, the following @DynamicProperty
definition uses https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL] with the current Bean as the https://docs.spring.io/spring-framework/reference/core/expressions/evaluation.html[root object] for the value annotation to add a property named messages.url
to the URL and the arbitrary available port of the CommonsExecWebServer
:
[source,java]
@Bean @DynamicProperty(name = "messages.url", value = "'http://localhost:' + port") static CommonsExecWebServerFactoryBean messagesApiServer() { return CommonsExecWebServerFactoryBean.builder() .classpath(cp -> cp .files("build/libs/messages-0.0.1-SNAPSHOT.jar") ); }
NOTE: While our @DynamicProperty
examples use CommonsExecWebServer
, the @DynamicProperty
annotation works with any type of Bean.
=== Composed @DynamicProperty
Annotations
@DynamicProperty
is treated as a meta-annotation, so you can create composed annotations with it.
For example, the following works the same as our example above:
.MessageUrl.java [source,java]
@Retention(RetentionPolicy.RUNTIME) @DynamicProperty(name = "message.url", value = "'http://localhost:' + port") public @interface MessageUrl { }
.Config.java [source,java]
@Bean @MessageUrl static CommonsExecWebServerFactoryBean oauthServer() { return CommonsExecWebServerFactoryBean.builder() .classpath(cp -> cp .files("build/libs/authorization-server-0.0.1-SNAPSHOT.jar") ); }
=== Well Known Composed @DynamicProperty
Annotations
This is a list of well known composed @DynamicProperty
annotations.
==== @OAuth2ClientProviderIssuerUri
This provides a mapping to issuer-uri of https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.security.spring.security.oauth2.client.provider[the OAuth provider details].
- name
spring.security.oauth2.client.provider.${providerName}.issuer-uri
with a defaultproviderName
ofspring
. TheproviderName
can be overridden with theOAuth2ClientProviderIssuerUri.providerName
property. - value
'http://127.0.0.1:' + port
which can be overriden with theOAuth2ClientProviderIssuerUri.value
property
== Samples Run xref:samples/oauth2-login/src/test/java/example/oauth2/login/TestOauth2LoginMain.java[TestOauth2LoginMain]. This starts the oauth2-login sample and a Spring Authorization Server you assembled in the previous step.
Visit http://localhost:8080/
You will be redirected to the authorization server.
Log in using the username user
and password password
.
You are then redirected to the oauth2-login application.