An open API service indexing awesome lists of open source software.

https://github.com/spring-projects-experimental/spring-boot-testjars


https://github.com/spring-projects-experimental/spring-boot-testjars

Last synced: 12 months ago
JSON representation

Awesome Lists containing this project

README

          

= spring-boot-testjars
:TESTJARS_VERSION: 0.0.3

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 <>
* <>

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+]
----

org.springframework.experimental.boot
spring-boot-testjars
{TESTJARS_VERSION}

----

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, add the following to your build:

.build.gradle
[source,groovy,subs=attributes+]
----
testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:{TESTJARS_VERSION}'
----

.pom.xml
[source,xml,subs=attributes+]
----

org.springframework.experimental.boot
spring-boot-testjars-maven
{TESTJARS_VERSION}

----

==== Custom Maven Repositories

By default, the following repositories are added:
- Maven Central
- If it is a SNAPSHOT dependency with a group id that starts with `org.springframework`, then Spring's milestone and snapshot repositories are added
- If it is a milestone or release candidate dependency with a group id that starts with `org.springframework`, then Spring's milestone repository is added

You can customize the repositories that are searched by injecting the repositories like the example below:

[source,java]
----
List repositories = new ArrayList<>();
repositories.add(new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build());
repositories.add(new RemoteRepository.Builder("sonatype-snapshot", "default", "https://oss.sonatype.org/content/repositories/snapshots/").build());
MavenClasspathEntry classpathEntry = new MavenClasspathEntry("org.junit:junit5-api:5.0.0-SNAPSHOT", repositories);
----

==== Exclude Transitive Dependencies

If you are resolving a Spring Boot (fat jar), then you do not need to resolve transitive dependencies.
To disable downloading transitive dependencies, you can pass in `true` for the `excludeTransitives` constructor argument.
For example:

[source,java]
----
boolean excludeTransitives = true;
List repositories = new ArrayList<>();
repositories.add(new RemoteRepository.Builder("example", "default", "https://example.org/maven2/").build());
MavenClasspathEntry classpathEntry = new MavenClasspathEntry("org.example:message-service:1.0.0", repositories, excludeTransitives);
----

=== GenericSpringBootApplicationMain

For adhoc applications, you may not have a main class and you may not want to provide boilerplate main method yourself.
If that is the case, you can leverage `useGenericSpringBootMain`:

[source,java]
----
@Bean
@OAuth2ClientProviderIssuerUri
static CommonsExecWebServerFactoryBean authorizationServer() {
// @formatter:off
return CommonsExecWebServerFactoryBean.builder()
// ...
// Add a generic class annotated with SpringBootApplication and a main method to the classpath and use it as the main class
.useGenericSpringBootMain();
// @formatter:on
}
----

=== Default Resources

If present, `CommonsExecWebServerFactoryBean` will add the resources `testjars/$beanName/**`, then it is automatically added to the classpath as resources renamed without the path of `testjars/$beanName`.
For example, if the bean name is `authorizationServer`, then the resource `testjars/authorizationServer/application.yml` will automatically be added to the classpath as `application.yml`.
Furthermore, a resource named `testjars/authorizationServer/foo/bar.txt` will automatically be added to the classpath as `foo/bar.txt`.

=== Adding Additional Classes to the ApplicationContext

It can often be helpful to add additional classes to the `ApplicationContext`.
Simply adding them to the classpath does not necessarily add the class to the `ApplicationContext`.
For example, if someone wants to start a Config Server instance, the `ConfigServerConfiguration` must be imported:

[source,java]
----
@Bean
@DynamicPortUrl(name = "spring.cloud.config.uri")
static CommonsExecWebServerFactoryBean configServer() {
// @formatter:off
return CommonsExecWebServerFactoryBean.builder()
.useGenericSpringBootMain()
.setAdditionalBeanClassNames("org.springframework.cloud.config.server.config.ConfigServerConfiguration")
.classpath((classpath) -> classpath
.entries(springBootStarter("web"))
.entries(new MavenClasspathEntry("org.springframework.cloud:spring-cloud-config-server:4.2.0"))
);
// @formatter:on
}
----

=== Debugging

If you need to start the application in debug mode, you can do so using the `DebugSettings`.

[source,java]
----
@Bean
@OAuth2ClientProviderIssuerUri
static CommonsExecWebServerFactoryBean authorizationServer() {
// @formatter:off
return CommonsExecWebServerFactoryBean.builder()
// ...
.debug((settings) -> settings
.enabled(true)
// Optional properties with their explicit defaults shown below
.suspend(true)
.port(5005)
);
// @formatter:on
}
----

When starting the remote debugger, it is important to remember that the classpath of the `CommonsExecWebServerFactoryBean` is independent of the project it runs in.
This means, the classpath of the debugger will need to match the classpat of the `CommonsExecWebServerFactoryBean` rather than the project it exists in.

=== Server Port

By default `CommonsExecWebServerFactoryBean` starts the application on a random port by specifying the system property `server.port=0`.
If you need to disable this behavior, you can opt out using the `useRandomPort` property as shown below:
[source,java]
----
@Bean
@OAuth2ClientProviderIssuerUri
static CommonsExecWebServerFactoryBean authorizationServer() {
// @formatter:off
return CommonsExecWebServerFactoryBean.builder()
// ...
.useRandomPort(false);
// @formatter:on
}
----

[[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.

=== @EnableDynamicProperty

In order to use `@DynamicProperty` annotations, it must be enabled with the `@EnableDynamicProperty` annotation as show below:

[source,java]
----
@Configuration
@EnableDynamicProperty
class MyConfiguration {
// ...
}
----

=== @DynamicProperty Example

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.

==== @DynamicPortUrl

This provides a simple way of mapping a property to a URL with a dynamic port that is expressed as the port property on the Bean that is created.
The value is calculated as `http://{host}:{port}{contextRoot}`.

* name - the property name to use
* host - the host to use (default is `localhost`)
* port - a valid SpEL expression that determines the port to use for the URL (default port)
* contextRoot - the context root to use (default is empty String)

==== @CloudConfigUri

This simplifies mapping the to the property `spring.cloud.config.uri`.
The value is calculated as `http://{host}:{port}{contextRoot}` such that:

* host - the host to use (default is `localhost`)
* port - a valid SpEL expression that determines the port to use for the URL (default port)
* contextRoot - the context root to use (default is empty String)

==== @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 default `providerName` of `spring`. The `providerName` can be overridden with the `OAuth2ClientProviderIssuerUri.providerName` property.
* value `'http://127.0.0.1:' + port` which can be overriden with the `OAuth2ClientProviderIssuerUri.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.