Showing posts with label maven. Show all posts
Showing posts with label maven. Show all posts

Sunday, September 15, 2024

Spring Code TIP-1: Get code coverage for the main method in Spring Boot application . . .

Setting up a new Spring boot project is made trivial by the Spring Initializer. IDEs like IntelliJ has integrated support for this as well. The application generated by this initializer contains three files under src.
    1) The application file (e.g. DemoAppliction.java) is Spring Boot main class that bootstraps the project.
    2) A properties file (application.properties) is the application configuration file.
    3) A test-case file (DemoApplictionTests.java). 

The main application file looks like:
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
The SpringApplication.run(DemoApplication.class, args) performs bootstrapping, creates application context, and runs the application.

The configuration file looks like:
spring.application.name=demo

The test-case looks like:
package com.example.demo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class DemoApplicationTests { @Test void contextLoads() { } }
This is in fact a simple yet very useful test class and I would keep it around. The is an integration test-case that ensures that the application context loads successfully. If there are any issues this test fails. So, this can be treated like a integration smoke-test for your application. I would rename that test method name to smokeTest_contextLoads().

Spring Code TIP - Code coverage

However, once you progress on with the application and have code coverage check applied using build system like maven and its plugins Surefire, Failsafe and JaCoCo plugins, the main method is left uncovered by this test-case. Little strange!

Having a main method in the application supported by integrated test-case, one expects that the main method is invoked by the test-case. The annotation @SpringBootTest by default has this turned off. Hence @SpringBootTest calls SpringApplication.run(DemoApplication.class, args) and not the main method. So, in order to get the main method called instead when the test-case is run, we need to explicitly set a property. Once this is set, the test-case invokes the main method and the main method gets the code coverage.

The property is shown below to get the test coverage for the main method.
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)

Not just for the code coverage, you might also want to use the useMainMethod property in scenarios where your application's main method performs additional setup or configuration that is necessary for your tests as well.

Summary

The useMainMethod property of the @SpringBootTest annotation allows you to control whether the main method of your application is invoked to start the Spring Boot application context during testing. By default, useMainMethod is set to UseMainMethod.NEVER, but you can set it to UseMainMethod.ALWAYS or UseMainMethod.WHEN_AVAILABLE to ensure that the main method is called during testing.

Tuesday, December 12, 2023

Spot-check Spotless . . .

Sometimes, overwhelming details are underwhelming. I happened to quickly explore Spotless for code formatting and checks. Spotless is the plugin to spot check. It's good that the README of Spotless upfront has separated Gradle and Maven documentation. I was looking to add maven support for a new Java Spring Boot 3.0 GraphQL project. But the Spotless Maven README documentation was overwhelming.

Quickly read through for few minutes to get an essence of it. Though there are so many details, what I wanted to get done seemed simple. Of course, practicality is always different. That's where experience is gained or comes from ;)

Environment: Java 20, Spring Boot 3.2.0, maven 3.9.6 on macOS Catalina 10.15.7

I quickly applied it to the project, and immediately ran into a couple of Gotchas. It took me couple of hours to explore and set it up finding issues and fixing settings. This post is just about those issues that I ran into.

Use Spaces, not Tabs in code

I prefer SPACES to TABS in code. It is good practice to use SPACE instead of TAB in code formatting. This cannot be a personal preference. But most of IDEs still come with TAB as default setting and many developers don't even pay attention to it. In code reviews, the formatting goes off due to SPACESs vs. TABs and is always annoying.

I use IntelliJ IDEA for my development. Whenever I install IntelliJ, the very first thing I would setup is to use spaces instead of tabs (Go to Preferences > Editor > Code Style > Java, and uncheck Use tab character). With that all code that I write will only use spaces and not tabs. The next thing is to setup to show whitespaces (Go to Preferences > Editor > Appearance, and check Show whitespaces), to easily distinguish Tabs and Spaces by this setting on.

Gotcha-1

The Spotless maven plugin documentation describes many details. The setting I wanted for code check was found at a couple of places in there under <indent> tag. At only one place the setting <spaces>true</spaces> is specified. Though I tried few different things like false for <tabs>, and true for <spaces> etc., the spotless:check maven goal was still suggesting to change code with spaces to tabs.

We also have to use google code style settings: eclipse-formatter.xml, that I took from the other project to use. This is described under Java > eclipse jdt in the spotless documentation. It's a google code style Java settings guide in xml format ;)

The setting id that is used for what I wanted was like: <setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="tab"/>. Changing that "tab" to "space" made spotless happy with spaces in code. ;)

Gotcha-2

Binding to maven phase : I should have read it carefully. At the minimum the <executions><execution><goal>check</goal></execution></execution> is required. Without this ./mvnw spotless:check works, but ./mvnw clean install which also runs verify will not run spotless check. So, for spotless check goal to be bound to maven verify goal, the above setting is required.

A sample spotless settings looks like this:

... <plugins> <!-- spotless https://github.com/diffplug/spotless --> <plugin> <groupId>com.diffplug.spotless</groupId> <artifactId>spotless-maven-plugin</artifactId> <version>${spotless-plugin.version}</version> <executions> <execution> <goals> <goal>check</goal> </goals> </execution> </executions> <configuration> <java> <endWithNewline /> <trimTrailingWhitespace /> <cleanthat> <version>2.18</version> <mutators> <mutator>AvoidInlineConditionals</mutator> <mutator>LiteralsFirstInComparisons</mutator> <mutator>UnnecessaryImport</mutator> <mutator>UnnecessaryModifier</mutator> <mutator>UseUnderscoresInNumericLiterals</mutator> <mutator>UseDiamondOperator</mutator> </mutators> </cleanthat> <eclipse> <version>4.26</version> <file>${project.basedir}/eclipse-formatter.xml</file> </eclipse> </java> </configuration> </plugin> ... </plugins>

💡 TIPS

Spotless handy maven goals
Check: ./mvnw spotless:check
Apply: ./mvnw spotless:apply

Conclusion

Any new exploration in Maven world never goes smooth for me. There are always bumps along the way. I do not like copy and paste at all. After all, if everything works in the first place when quick-copy-paste-tool is used, there is not much left to learn ;)  With the Generative AI getting trained to write code, learning will soon become a rare thing!

References

Thursday, November 30, 2023

Docker maven plugin - Spring Boot, Redis : Gotcha

The docker-maven-plugin comes in handy for managing Docker images and containers in integration tests. It can be used to build images or run. Multiple images can be configured to be built or run depending on the need of your application. This post focuses on run aspect of this plugin; specifically running Redis image in Docker container and a potential issue that one might run into.

Environment: Java 20, Spring Boot 3.1.5, maven 3.8.6 on macOS Catalina 10.15.7

The Scenario

After going through some painful hoops, I upgraded a Spring Boot application from 2.6.3 to 3.1.5 with the two-step recommended approach 2.6.x to 2.7.x and then to 3.1.x. The application uses Redis for caching needs. All worked well at the end. The app was built through concourse CI/CD pipeline and successfully deployed as Kubernetes workload to int and cert environments and has been successfully running for few weeks.

I started to work on fixing some critical and high Snyk reported open-source dependency vulnerabilities. Suddenly, integration tests around Redis started to fail with the following exception:

[ERROR] com.hmhco.api.assessmentservice.service.AssessmentServiceCacheIT.testCache -- Time elapsed: 0.478 s <<< ERROR! org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1604) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1535) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getNativeConnection(LettuceConnectionFactory.java:1360) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getConnection(LettuceConnectionFactory.java:1343) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getSharedConnection(LettuceConnectionFactory.java:1061) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getConnection(LettuceConnectionFactory.java:400) at org.springframework.data.redis.cache.DefaultRedisCacheWriter.execute(DefaultRedisCacheWriter.java:272) at org.springframework.data.redis.cache.DefaultRedisCacheWriter.clean(DefaultRedisCacheWriter.java:189) at org.springframework.data.redis.cache.RedisCache.clear(RedisCache.java:220)

The above exception stack-trace didn't give much clue. After looking into few things including redis test configurations, dependencies including transitive dependencies etc. it was still puzzling as it was actually working few weeks ago, and all of sudden only locally the integration test-cases around redis started to fail.

The docker-maven-plugin actually logs that redis:latest started, before running integration tests like below:

... [INFO] DOCKER> [redis:latest] "redis-test": Start container 395afa51913b ...

The last thing I looked at was the docker containers:

$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 395afa51913b redis "docker-entrypoint.s…" About a minute ago Exited (1) About a minute ago redis-1 b03c67ae628e postgres:12.4 "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5433->5432/tcp postgres-1

That showed like the container started but exited for some reason. I also looked at the for Docker Dashboard for any additional details.


Clicking redis-1 container that exited shows logs and the reason: # Fatal: Can't initialize Background Jobs. Error message: Operation not permitted
Also gives Redis version detail: 7.2.3


That's the issue. Redis started but exited and hence the test failed with the exception - Unable to connect to Redis 

After googling about that initialization issue (# Fatal: Can't initialize Background Jobs. Error message: Operation not permitted), people recommended to try few specific Redis versions. After some trail and error, I had luck with version 6.2.6.

The logs can also be fetched from command-line by executing for the CONTAINER ID (395afa51913b ) or the NAME (redis-1):
$ docker logs 395afa51913b

The Fix

In the docker-maven-plugin configuration, the image tag can be specified for specific version 6.2.6 as shown below:

<properties> <redis.test.port>6381</redis.test.port> </properties> ... <plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.43.4</version> <executions> <execution> <id>start</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <images> <image> <build> <cleanup>true</cleanup> <tags> <tag>latest</tag> </tags> </build> <external> <type>properties</type> <prefix>postgres.docker</prefix> </external> </image> <image> <name>redis:6.2.6</name> <alias>redis-test</alias> <run> <ports> <port>${redis.test.port}:6379</port> </ports> </run> </image> </images> </configuration> </plugin>

TIP

Testcontainers is another choice to look into for integration tests.

GOTCHA

It's Gotcha in a Gotcha post ;) I always run into these kinds of issues whenever I explore anything anytime in maven world.

The docker-maven-plugin docker goals start and stop are bound to maven failsafe plugin pre-integration-test and post-integration-test phases of integration-test goal. But if you skip tests by passing argument -DskipTests which actually would skip running all unit and integration tests, I expected docker images not to be run. For instance ./mvnw clean install -DskipTests command to compile, build, package and install all modules by skipping tests. However, in this case, the images do get run: started and stopped. I couldn't find a way to get a hold on this, could be a bug or issue in the plugin by design, not sure.

That was happening right after spring-boot-maven-plugin's package goal. That plugin also gets bound to pre-integration-test and post-integration-test phases when specified. We don't have these executions specified for this plugin though.

Couldn't figure out a workaround for not getting docker images run when integration test cases are skipped. If anyone has a solution for this, please post a comment, I would appreciate it.

The word trivial in software development is actually complex. Things are unnecessarily made super-complex. Software developers get paid for dealing with it anyways ;)

References

Monday, August 28, 2023

IntelliJ - the Community Edition rescued the broken Ultimate Edition . . . ;)

It sound funny to say that - IntelliJ Community Edition (free edition) rescued Ultimate Edition (paid edition). But that's what really put me back on Ultimate Edition after many months of switching to Community Edition for my day-to-day development.

Environment: IntelliJ Community Edition 2023.2.1IntelliJ Ultimate Edition 2023.2.1, maven 3.9.3 on macOS Catalina 10.15.7

The Issue

I stopped using IntelliJ Ultimate Edition as for some weird reason it got stuck with maven dependencies broken paths issue. Specifically, SLF4J and javax libraries were shown in maven dependency broken paths in red. It happened few months ago. One day one of my maven projects that uses Lombok and had classes annotated with @Slf4J suddenly couldn't recognize the log (logger object) statements. When I checked project module dependencies there were broken maven dependency paths.

I tried all recommended and possible ways to recover from this issue like: Invalidate Caches and restart the IDE(A), blowing away specific libraries maven cache ~/.m2/libraries/.../*.* and letting it build again, blowing away entire maven local cache ~/.m2/*.* and letting it build again, downloading dependency jars and explicitly setting dependency paths in the project/module settings, reinstalling the Ultimate Edition, even reinstalling different version of the Ultimate Edition etc. Nothing worked. I spent lot of time few times since then and couldn't get it back to working. Finally gave up and moved to Community Edition. Same projects that were having broken dependency issues in Ultimate Edition, when opened in Community edition, had no issues with those dependencies, same maven local cache paths are used by both. The Ultimate Edition complains, the Community Edition doesn't. Alas!

The Fix

Here is what I did to fix it.

Started IntelliJ Ultimate Edition. At the startup there is a Customize link, click that and click Import Settings... link as shown below:

Selected the Community Edition settings directory (~/Library/Application Support/JetBrains/IdeaIC2023.2) of Community Edition in which my projects were fine with dependencies. It prompted to take a backup of current settings. I did that and imported ommunity Edition settings. It opened projects and the issue of broken dependency paths was gone.

It seemed like, there was some broken dependency path setting saved in the Ultimate Edition settings that was stuck and not getting fixed by any means.

Some Internals

IntelliJ saves all it's settings under the specific version's area. On Mac, by default, this area is under ~/Library/Application Support/JetBrains dir. The community edition directories start with IdeaIC<version> (e.g IdeaIC2023.2) and ultimate edition directories start with IntelliJIdea<version> (e.g. IntelliJIdea2023.2). The options sub-directory is where plugins settings get saved. 

Plugin Settings

Plugin settings are pat of IntelliJ settings and get stored in xml files under options sub-directory of specific IntelliJ version's settings directory. For instance, awesome editor plugin is a simple and pretty neat plugin which lets add image backgrounds. I did setup plugin awesome editor to display different kinds of images for different types of files. To get all the settings from one IntelliJ version to another, just copy the plugin settings file (in this case it is: awesome-editor-3.xml). 

Here is an example to copy plugin settings set in Ultimate edition to Community Edition.
NOTE: Once copied restart IntelliJ Community Edition.

$ cd ~/Library/"Application Support"/JetBrains $ ls -al |grep IdeaIC drwxr-xr-x 17 pottepalemg 163264107 544 Jul 26 2022 IdeaIC2022.1 drwxr-xr-x 20 pottepalemg 163264107 640 Nov 30 2022 IdeaIC2022.2 drwxr-xr-x 20 pottepalemg 163264107 640 Mar 30 15:23 IdeaIC2022.3 drwxr-xr-x 20 pottepalemg 163264107 640 Jul 7 11:31 IdeaIC2023.1 drwxr-xr-x 20 pottepalemg 163264107 640 Aug 28 10:13 IdeaIC2023.2 $ ls -al |grep IntelliJIdea drwxr-xr-x 22 pottepalemg 163264107 704 Aug 24 15:43 IntelliJIdea2020.1 drwxr-xr-x 19 pottepalemg 163264107 608 Nov 19 2020 IntelliJIdea2020.2 drwxr-xr-x 21 pottepalemg 163264107 672 Mar 2 2022 IntelliJIdea2020.3 drwxr-xr-x 20 pottepalemg 163264107 640 Jul 9 2021 IntelliJIdea2021.1 drwxr-xr-x 24 pottepalemg 163264107 768 Feb 9 2022 IntelliJIdea2021.2 drwxr-xr-x 25 pottepalemg 163264107 800 Apr 12 2022 IntelliJIdea2021.3 drwxr-xr-x 26 pottepalemg 163264107 832 Sep 23 2022 IntelliJIdea2022.1 drwxr-xr-x 25 pottepalemg 163264107 800 Nov 30 2022 IntelliJIdea2022.2 drwxr-xr-x 25 pottepalemg 163264107 800 Jun 28 13:26 IntelliJIdea2022.3 drwxr-xr-x 24 pottepalemg 163264107 768 Aug 16 15:40 IntelliJIdea2023.1 drwxr-xr-x 22 pottepalemg 163264107 704 Aug 28 10:36 IntelliJIdea2023.2 drwxr-xr-x 11 pottepalemg 163264107 352 Aug 25 15:35 IntelliJIdea2023.2-backup $ find . -name awesome* ./IntelliJIdea2023.2/options/awesome-editor-3.xml $ ls -ltr ./IdeaIC2023.2/options | grep awesome* $ cp ./IntelliJIdea2023.2/options/awesome-editor-3.xml ./IdeaIC2023.2/options

TIPS

Getting back lost database connection settings into Ultimate Edition

IntelliJ Ultimate Edition comes bundled with Database plugin that supports all features that are available in DataGrip (an SQL IDE which is a product of JetBrains). I had database connections set to connect to various PostgreSQL databases (local, int, cert, prod etc.) which I lost by importing Community Edition settings. But I had a settings backup prompted and done for the Ultimate Edition settings that my broken Ultimate Edition was setup with when I imported Community Edition Settings. The backup directory is also listed in the above list of Ultimate Editions setting directories. To get those settings back onto my new settings imported from Community Edition, I had to repeat the Customize > Import Settings... step two more times. First time pointing it to the backup directory and selecting and copying all database connections settings to the clipboard. Second time pointing it to the Community Edition Directory and pasting those connection settings from the clipboard. This was the only way I could get those database connections copied. I got all connection settings except passwords fro every connection. I had to set password for every single connection individually. This was not possible by simply copying xml files like I did for awesome editor plugin.

Summary

No software application or tool is bug-free. Applications do crash, tools do get corrupted. There is no one solution that works or fixes a similar issue for everybody. Some stupid, nasty, unknown, not very well documented internals of tools do take up lot of time to discover a fix that works for your situation and may help some others who get into similar situation.

Hope this blog post on my discovery saves someone's time sometime when that someone bumps into it.

Friday, August 11, 2023

Spring boot - your own banner, actuator, version details etc . . .

Art is good for eyes. Spring boot out of the box comes with a nice text banner of it's name : Spring Boot and displays the version right below the banner. Out of the box, the banner mode is set on and it shows up when the application gets started. There are several articles available on customizing this banner. Also, spring boot documentation has a brief section on thus as well (link in the resources).

Environment: Java 20, Spring Boot 2.7.14, maven 3.9.3 on macOS Catalina 10.15.7

It's always good to customize the banner and see your application name whenever it comes up. Of course, you can display more good-to-have version details like: Spring Boot version, Java version, Your application version, and other available properties along with the banner.

Following is the quick list of steps to have a custom banner in your application.
  • Generate a text banner for your application name. There are several sites for doing this. The one such is: https://devops.datenkollektiv.de/banner.txt/index.html. Generate a text banner and download the text file. I usually go with the standard banner font.
  • Edit the file and add the following properties for versions at the bottom:
____ _ ____ ___ __ | __ ) ___ ___ | |_ / ___|_ __ __ _ __ _| \ \ / / __ ___ | _ \ / _ \ / _ \| __| | | _| '__/ _` |/ _` | |\ \ / / '_ ` _ \ | |_) | (_) | (_) | |_ | |_| | | | (_| | (_| | | \ V /| | | | | | |____/ \___/ \___/ \__| \____|_| \__,_|\__,_|_| \_/ |_| |_| |_| :: Spring Boot :: ${spring-boot.version} :: Running on Java :: ${java.version} :: Application :: ${project.version}
  • Place the text file with file-name: banner.txt under src/java/resources folder.
  • In your maven build file (pom.xml), make sure that you turn on maven filtering and add the resource directory for the extra version properties added to be resolved during the build.
  • Also, make sure you have maven-resources-plugin configured to filter resources.
<build> <resources> <resource> <filtering>true</filtering> <directory>${project.basedir}/src/main/resources/</directory> </resource> </resources> <testResources> <testResource> <filtering>true</filtering> <directory>${project.basedir}/src/test/resources/</directory> </testResource> </testResources> ... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <executions> <execution> <id>filter-resources</id> <phase>process-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <resources> <!-- # Filtered Resources --> <resource> <directory>${project.basedir}/src/main/resources/</directory> <filtering>true</filtering> </resource> </resources> <outputDirectory>${project.build.directory}</outputDirectory> </configuration> </execution> </executions> </plugin> ... </build>

Gotcha-1

When you run maven build goal(s) that also runs test-cases like for e.g. ./mvnw clean install, the banner shows up with all properties resolved for every integration test-case. However, for the custom properties used in the banner to be filtered and shown you need to add the same custom property in application.yml if you happen to separate out test configurations under test/resources. Also, make sure that you have enabled <testResources> filtering as well as shown above.

Gotcha-2 (Spring Boot 3.x)

The above ${project.version} doesn't work in Spring boot 3.x. In this case a custom application version property (e.g. app.version) can be defined in application.yml or application.properties and that can be used in the banner.txt file.
E.g. application.yml
spring: application: name: @project.name@ # custom property for banner app: version: @project.version@

____ _ ____ ___ __ | __ ) ___ ___ | |_ / ___|_ __ __ _ __ _| \ \ / / __ ___ | _ \ / _ \ / _ \| __| | | _| '__/ _` |/ _` | |\ \ / / '_ ` _ \ | |_) | (_) | (_) | |_ | |_| | | | (_| | (_| | | \ V /| | | | | | |____/ \___/ \___/ \__| \____|_| \__,_|\__,_|_| \_/ |_| |_| |_| :: Spring Boot :: ${spring-boot.version} :: Running on Java :: ${java.version} :: Application :: ${app.version}

Actuator

Actuator provides production-ready endpoints for monitoring the application. Just adding the following dependency in pom.xml will do. Once the application is up, check http://localhost:8080/actuator

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>

However, by default not all end-points are enabled. Only /actuator, and /actuator/health end-points are enabled. By setting the property management.endpoints.web.exposure.include=* in application.properties or corresponding application.yml will activate all end-points. 

Also, multiple end-points can be listed separated by comma.
e.g. management.endpoints.web.exposure.include=health,info,beans,env

Selectively certain end-points can be disabled, for instance to disable refresh end-point, set management.endpoints.web.exposure.exclude=refresh 

NOTE: The refresh end-point requires an empty POST request. e.g. curl -X POST http://localhost:8081/actuator/refresh 

Application information

The /actuator/info endpoint displays application information. By default there is no information. So, http://localhost:8080/actuator/info displays empty JSON:
{}

Maven plugin - spring-boot-maven-plugin

This plugin comes with build execution goal: build-info which is run by default and creates build information file: build-info.properties under target/classes/META_INF directory. Properties listed in this file are available through the endpoint: http://localhost:8080/actuator/info.
The following is an example of build-info goal execution configuration:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <!-- Useful info on /actuator/info --> <id>build-info</id> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin>

The above configuration generates build-info.properties file with pre-defined build info properties like:
build.artifact=boot-graalvm build.group=com.example build.name=boot-graalvm build.time=2023-09-22T16\:04\:27.970Z build.version=0.0.1-SNAPSHOT

With the above file generated, the /actuator/info endpoint response would look like:
{ "build": { "artifact": "boot-graalvm", "name": "boot-graalvm", "time": "2023-09-22T16:04:27.970Z", "version": "0.0.1-SNAPSHOT", "group": "com.example" } }
 
Additional custom properties can be added by configuring the build-info goal. For instance to add Java version and the Spring Boot version that the application is running with, the following additional configuration can be added:
... <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>boot-graalvm</artifactId> <version>0.0.1-SNAPSHOT</version> <name>boot-graalvm</name> <description>GraalVm project for Spring Boot</description> <properties> <java.version>20</java.version> <spring.boot.version>${parent.version}</spring.boot.version> </properties> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <!-- Useful info on /actuator/info --> <id>build-info</id> <goals> <goal>build-info</goal> </goals> <configuration> <additionalProperties> <java.version>${java.version}</java.version> <spring.boot.version>${spring.boot.version}</spring.boot.version> </additionalProperties> </configuration> </execution> </executions> ...

Which would result with additional properties in the build-info.properties file like:
build.java.version=20 build.spring.boot.version=3.1.2

The /actuator/info endpoint response looks like:
{ "build": { "java": { "version": "20" }, "spring": { "boot": { "version": "3.1.2" } }, "version": "0.0.1-SNAPSHOT", "artifact": "boot-graalvm", "name": "boot-graalvm", "time": "2023-09-22T18:52:39.193Z", "group": "com.example" } }

Java information

Info endpoint has several info contributors like build, env, java, git etc. By default they are disabled. Enabling these would show related information collected by Spring.

To enable java info, add management.info.java.enabled=true in application.properties or management.info.java.enabled:true in application.yml.

This will show the following java info details collected like:
{ "build": { "java": { "version": "20" }, "spring": { "boot": { "version": "3.1.2" } }, "version": "0.0.1-SNAPSHOT", "artifact": "boot-graalvm", "name": "boot-graalvm", "time": "2023-09-22T18:52:39.193Z", "group": "com.example" }, "java": { "version": "20", "vendor": { "name": "Amazon.com Inc.", "version": "Corretto-20.0.0.36.1" }, "runtime": { "name": "OpenJDK Runtime Environment", "version": "20+36-FR" }, "jvm": { "name": "OpenJDK 64-Bit Server VM", "vendor": "Amazon.com Inc.", "version": "20+36-FR" } } }

TIP

These build properties generated and available through /actuator/info are also are available through BuildProperties object which can be auto-wired into any of the Spring managed beans and be accessed. For instance these properties can be outputted on swagger-ui page.


References

Saturday, August 05, 2023

Java bytecode - compiler version options and compatibilities . . .

One of many strengths of Java platform is its backward compatibility with the language. As language keeps evolving and moving forward, the good old syntax is still supported for backward compatibility. However, the compiler adds certain indicative options for specifying version details. The --source, --target are two such compiler (javac) options. From Java 9 onwards a third option --release got added to this mix. Getting a good understanding of these options is not trivial without actually experiencing all three. When compiling source code of a single class you may not need to specify these options. But in Java project when building with maven like build system, one needs to understand these options and their implications.

Environment: Java 20, Spring Boot 2.7.15, maven 3.9.3 on macOS Catalina 10.15.7

The maven-compiler-plugin

Maven build system uses maven-compiler-plugin for compiling source code. This plugin documentation upfront talks about source and target options and highly recommends to change these in plugin configuration. In order to change these per application/module needs, one needs to look under the hood for understanding.

Various extra Java compiler options can be specified in the maven-compiler-plugin configuration. The actual Java compiler options related to version are: --source, --target and --release that can be specified and passed to the compiler during code compilation through maven-compiler-plugin configuration. This can be done in two different ways in pom.xml:

1. Through maven properties: maven.compiler.source,  maven.compiler.target and maven.compiler.release as highlighted below:

<properties> <maven.compiler.source>20</maven.compiler.source> <maven.compiler.target>20</maven.compiler.target> <maven.compiler.target>20</maven.compiler.target> </properties>

If these properties are not explicitly defined, maven compiler plugin uses 1.8 for source and target.

2. Through the plugin configuration settings as highlighted below. Note - For convenience defined extra properties and used for source, target and release but straight version numbers can be used.

... <properties> <java.version>20</java.version> <javac.source.version>${java.version}</javac.source.version> <javac.target.version>${java.version}</javac.target.version> <javac.release.version>${java.version}</javac.release.version> <!-- Maven plugins --> <maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version> </properties> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>${javac.source.version}</source> <target>${javac.target.version}</target> <release>${javac.release.version}</release> <compilerArgs> <arg>-Xlint:all</arg> </compilerArgs> </configuration> </plugin> </plugins> </pluginManagement> ...

If no special configuration is required, it doesn't require even to specify maven-compiler-plugin. If specified, the above are two ways to control/change default 1.8 set by the plugin for these options which eventually get passed to the Java compiler (javac) during code compilation of sources (both under src and test

Note from Java 9 onwards, the values to these options are not like 1.7, 1.8 but must be 7 and 8.

Java 20

Java 20 compiler doesn't support version 7 for source, target anymore. The supported releases are 8 through 20. So, for any reason if maven compiler plugin is set explicitly with 1.7, build fails with ERRORS saying: Source option 7 is no longer supported. Use 8 or later. , and Target option 7 is no longer supported. Use 8 or later. 

Now it's time to understand what these options actually tell the compiler, javac. The compiler's help option (javac -help) lists all available options and a brief description about each option. The -source, -target, -release options descriptions are helpful to some extent.

--source <release>, -source <release> Provide source compatibility with the specified Java SE release. Supported releases: 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 --target <release>, -target <release> Generate class files suitable for the specified Java SE release. Supported releases: 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 --release <release> Compile for the specified Java SE release. Supported releases: 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20

To understand these compiler options better, we can compile a simple Java application class with just main method.

HelloJava.java
import java.util.Properties; public class HelloJava { public static void main(String[] args) { Properties systemProperties = System.getProperties(); System.out.println(String.format("Hello Java %s!", systemProperties.getProperty("java.vm.specification.version"))); systemProperties.entrySet().stream() .filter(entry -> entry.getKey().toString().startsWith("java")) .toList().stream() .forEach(entry -> System.out.println(entry.getKey() + "=" + systemProperties.getProperty(entry.getKey().toString())) ); } }
Note - The above class prints system properties that start with "java" with their values. It uses toList() method that Java 16 added to Stream class. With this the expectation is- the code should not be compiled for Java/JVM version less than 16.

Java compiler version options

Let's compile the class with different Java versions and compiler options.
 
// compile with Java 20: no options specified $ sdk use java 20.0.2-amzn // check: major version $ javap -verbose HelloJava.class | grep major major version: 64 // run on Java 20: works $ javac HelloJava.java "Hello Java 20!" // switch to Java 17 and run: fails with LinkageError $ sdk use java 17.0.1.12.1-amzn $ java HelloJava Error: LinkageError occurred while loading main class HelloJava java.lang.UnsupportedClassVersionError: HelloJava has been compiled by a more recent version of the Java Runtime (class file version 64.0), this version of the Java Runtime only recognizes class file versions up to 61.0 // switch to Java 15 and run: fails with LinkageError $ sdk use java 15.0.2.7.1-amzn $ java HelloJava Error: LinkageError occurred while loading main class HelloJava java.lang.UnsupportedClassVersionError: HelloJava has been compiled by a more recent version of the Java Runtime (class file version 64.0), this version of the Java Runtime only recognizes class file versions up to 59.0 // switch to Java 8 and run: fails with UnsupportedClassVersionError Exception $ sdk use java 8.0.352-amzn $ java HelloJava Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.UnsupportedClassVersionError: HelloJava has been compiled by a more recent version of the Java Runtime (class file version 64.0), this version of the Java Runtime only recognizes class file versions up to 52.0 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:756) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:473) at java.net.URLClassLoader.access$100(URLClassLoader.java:74) at java.net.URLClassLoader$1.run(URLClassLoader.java:369) at java.net.URLClassLoader$1.run(URLClassLoader.java:363) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:362) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601)

So. when compiled with a specific version of java compiler (in this case Java 20 and no version options are specified), it gets compiled with default target of the Java compiler which is 20. The class generated cannot be run on prior JVM versions (prior to 20). To find the target of JVM code the byte-code is generated for, use javap -verbose HelloJava.class | grep major.

Now, let's try compiling for target 17 and try to run on different JVM versions.

// compile with Java 20 for target 17: --source and --target options specified $ sdk use java 20.0.2-amzn $ javac --source=17 --target=17 HelloJava.java warning: [options] system modules path not set in conjunction with -source 17 1 warning // check: major version $ javap -verbose HelloJava.class | grep major major version: 61 // run on Java 20: works $ java HelloJava Hello Java 20! // switch to Java 17 and run: works $ sdk use java 17.0.1.12.1-amzn // run on Java 17: works $ java HelloJava Hello Java 17! // swicth to Java 15 and run: fails with LinkageError $ sdk use java 15.0.2.7.1-amzn $ java HelloJava Error: LinkageError occurred while loading main class HelloJava java.lang.UnsupportedClassVersionError: HelloJava has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 59.0 // compile with Java 20 for target 17: --source and --target options specified $ sdk use java 20.0.2-amzn $ javac --source=15 --target=15 HelloJava.java warning: [options] system modules path not set in conjunction with -source 15 1 warning // check: major version $ javap -verbose HelloJava.class | grep major major version: 59 // switch to Java 17 and run: works $ sdk use java 17.0.1.12.1-amzn $ java HelloJava Hello Java 17! // swicth to Java 15 and run: fails with NoSuchMethodError $ sdk use java 15.0.2.7.1-amzn $ java HelloJava Hello Java 15! Exception in thread "main" java.lang.NoSuchMethodError: 'java.util.List java.util.stream.Stream.toList()' at HelloJava.main(HelloJava.java:15) // switch to Java 8 and run: fais with UnsupportedClassVersionError $ sdk use java 8.0.352-amzn $ java HelloJava Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.UnsupportedClassVersionError: HelloJava has been compiled by a more recent version of the Java Runtime (class file version 59.0), this version of the Java Runtime only recognizes class file versions up to 52.0 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:756) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:473) at java.net.URLClassLoader.access$100(URLClassLoader.java:74) at java.net.URLClassLoader$1.run(URLClassLoader.java:369) at java.net.URLClassLoader$1.run(URLClassLoader.java:363) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:362) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601) // compile with Java 20 for target 8: --source and --target options specified $ sdk use java 20.0.2-amzn $ javac --source=8 --target=8 HelloJava.java javac --source=8 --target=8 HelloJava.java warning: [options] bootstrap class path not set in conjunction with -source 8 warning: [options] source value 8 is obsolete and will be removed in a future release warning: [options] target value 8 is obsolete and will be removed in a future release warning: [options] To suppress warnings about obsolete options, use -Xlint:-options. 4 warnings // check: major version $ javap -verbose HelloJava.class | grep major major version: 52 // switch to Java 20 and run: works $ sdk use java 20.0.2-amzn $ java HelloJava Hello Java 20! // switch to Java 17 and run: works $ sdk use java 17.0.1.12.1-amzn $ java HelloJava Hello Java 17! // swicth to Java 15 and run: fails with NoSuchMethodError $ sdk use java 15.0.2.7.1-amzn $ java HelloJava Hello Java 15! Exception in thread "main" java.lang.NoSuchMethodError: 'java.util.List java.util.stream.Stream.toList()' at HelloJava.main(HelloJava.java:15)

So, when compiled for a target version, it cannot be run on prior JVM versions.

Let's try target 7.
$ sdk use java 20.0.2-amzn $ javac --source=7 --target=7 HelloJava.java warning: [options] bootstrap class path not set in conjunction with -source 7 error: Source option 7 is no longer supported. Use 8 or later. error: Target option 7 is no longer supported. Use 8 or later.
Java version 7 is not supported anymore.

Let's experience --release option.
// compile with Java 20 using options --source and --target options specified $ sdk use java 20.0.2-amzn $ javac --source=17 --target=17 --release=17 HelloJava.java error: option --source cannot be used together with --release error: option --target cannot be used together with --release Usage: javac <options> <source files> use --help for a list of possible options // compile with Java 20 for target 17: --release options specified $ sdk use java 20.0.2-amzn $ javac --release=17 HelloJava.java // check: major version $ javap -verbose HelloJava.class | grep major major version: 61 // switch to Java 20 and run: works $ sdk use java 20.0.2-amzn $ java HelloJava Hello Java 20! // switch to Java 17 and run: works $ sdk use java 17.0.1.12.1-amzn $ java HelloJava Hello Java 17! // switch to Java 15 and run: fails with NoSuchMethodError $ sdk use java 15.0.2.7.1-amzn $ java HelloJava Hello Java Exception in thread "main" java.lang.NoSuchMethodError: 'java.util.List java.util.stream.Stream.toList()' at HelloJava.main(HelloJava.java:14) // compile with Java 20 for target 15: --release options specified $ sdk use java 20.0.2-amzn $ javac --release=15 HelloJava.java HelloJava.java:14: error: cannot find symbol .toList().stream() ^ symbol: method toList() location: interface Stream<Entry<Object,Object>> 1 error // compile with Java 20 for target 15: --source --target options specified $ sdk use java 20.0.2-amzn $ javac --source=15 --target=15 HelloJava.java warning: [options] system modules path not set in conjunction with -source 15 1 warning // switch to Java 15 and run: fails with NoSuchMethodError $ sdk use java 15.0.2.7.1-amzn $ java HelloJava Exception in thread "main" java.lang.NoSuchMethodError: 'java.util.List java.util.stream.Stream.toList()' at HelloJava.main(HelloJava.java:14)

So, --release option does a strict compilation time checks to see if the code is compliant with the release target and fails to compile if a method not supported in target version is used in the code. This makes sure the compiled class works on target release (the target JVM version that the code is released to run on). Whereas, the --target option doesn't do code compliance checks during compilation time, it simply compiles code but fails during runtime. So, --release seems like better option to leverage when specifying.

Implications of version options

  • No compiler options specified: The code gets compiled with default target as the version of the Java compiler.
  • All 3 options  --source, --target and --release specified:  Not allowed.
  • Options --source--target specified: 1) Both can be same (e.g. 17, 17). 2) The option: --source can be lower version (e.g. 15) and --target can be higher version (e.g. 17), but not the other way. If --source is higher version (e.g.17) and --target is lower version (e.g.15), compiler fails with a warning: warning: source release 17 requires target release 17, it doesn't get compiled.
  • Only option --source: The option --source can be any version but default target would be the version of Java compiler being used.
  • Only option --target: The option: --target cannot be lower than the version of Java compiler being used because default source would be the version of the compiler being used. A lower target version gets into compiler option source higher, target lower and fails compilation. E.g javac --target=19 HelloJava.java with Java 20 compiler fails with warning: target release 19 conflicts with default source release 20 and code doesn't get compiled.
  •  Only option --release: Strict code check during compilation to make sure that compiled code gets compiled and works on the target version. Also, the byte-code generated runs only on the release specified and higher, but doesn't run on any lower versions.
    • Compiling using Java 20, and no --release option or --release=20 results with major version 64 (Java 20).
    • Compiling using Java 20 with --release=19 results with major version 63 (Java 19).
    • Compiling using Java 20 with --release=17 results with major version 61 (Java 17) and would result with LinkageError when run on lower version other than 17. Works on 17 and higher.

The maven-compiler-plugin variations with these options

Maven, out of the box with no maven-compiler-plugin specified in pom.xml, has the following variations with it's compiler version option properties (maven.compiler.source, maven.compiler.target and maven.compiler.release).

<properties> <maven.compiler.source>20</maven.compiler.source> <maven.compiler.target>20</maven.compiler.target> <maven.compiler.release>20</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
  • No version properties are specified: (No maven.compiler.source, maven.compiler.target and maven.compiler.release properties): Build fails with compilation ERRORS: Source option 5 is no longer supported. Use 8 or later. and Target option 5 is no longer supported. Use 8 or later.
  • All 3 version properties are specified: The option: maven.compiler.release is ignored, it can be any junk. Only source and target properties matter. The value for target option cannot be less than the source. For instance, source 20, target 19 fails build with warning: source release 20 requires target release 20.
  • Only source version property is specified: When only source is specified, target must also be specified. Otherwise, maven build fails with: Fatal error compiling: warning: source release 20 requires target release 20
  • Only target version property is specified: When only target is specified, source must also be specified. Otherwise, maven build fails with: Source option 5 is no longer supported. Use 8 or later.
  • The release property specified: When release is specified and is 8 or later, this takes the precedence. Make a special NOTE of it. When release is specified, it takes the precedence and sourcetarget options are ignored. In this case, any non-sense value will make the build work and code gets compiled for the release version specified. But this must be 8 or later.
With maven-compiler-plugin specified in pom.xml, has the following variations with these options specified in the plugin configuration (<configuration>): 

<properties> <maven.compiler.source>20</maven.compiler.source> <maven.compiler.target>20</maven.compiler.target> <maven.compiler.release>20</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> ... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>20</source> <target>20</target> <release>20</release> </configuration> </plugin> </plugins> </build>

With the above way of having both defined set of properties, and maven-compiler-plugin configuration, the configuration values override the property values defined. If no configuration is specified for the maven-compiler-plugin, it uses the set of properties defined. With configuration <source>, <target>, and <release> taking the precedence, the variations are as follows:
  • No <configuration> specified for the plugin, but set of properties are defined: It uses defined set of properties if exist, release takes the precedence over target and source.
  • No properties are defined, and no <configuration> is specified for the plugin : It defaults to target 1.8.
  • All 3 configuration options are specified: The release configuration takes the precedence and is built for the release version.
  • No properties are set, and only source configuration option is specified: When only source is specified, target must also be specified. Otherwise, maven build fails with: Fatal error compiling: warning: source release 20 requires target release 20
  • No properties are set, and only target configuration option is specified: Code gets compiled for the target specified.
  • No properties are set, and both target and release configuration options are specified: The release option takes the precedence and code gets compiled for the release specified.

Summary

For Java 9 and after, use --release option.
For older versions prior to Java 9 use --source and --target options.

TIPS

  • Use SDKMAN to install multiple Java versions and easily switch between different versions.
  • If You see noisy warning: Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection:, then add compiler argument <arg>-parameters</arg> to the maven-compiler-plugin configuration as shown below:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>${javac.source.version}</source> <target>${javac.target.version}</target> <release>${javac.release.version}</release> <compilerArgs> <arg>-Xlint:all</arg> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>

References

Wednesday, May 25, 2022

Maven - finer control of running tests . . .

There should always be something new to learn everyday. Otherwise, life is boring. In software development, it is even more boring if we don't explore to learn anything new.

Maven is popular open source build tool but comes with a price tag of "Time", consumes much of your time - the most precious of all. I've just leaned a better way to take a finer control of running specific unit/integration test. Two years ago, when I was back to Maven-Java world from Gradle-Groovy/Grails world, I had to do quite a bit of exploration. I still end up doing now and then even after a couple of years. Welcome to the Maven world ;)

Environment: Java 18, maven 3.8.5 on macOS Catalina 10.15.7

Surefire and Failsafe are maven plugins used for unit and integration tests. Out-of-the-box, with no additional configuration, you can simply skip running all tests by passing the command line argument to define the property skipTests like :  -DskipTests. This skips all tests, both unit and integration which is also same setting it to true: -DskipTests=true. So, specifying or specifying by setting it with true is same. By default, this property is set with false. So, not specifying at all on the command line, or specifying it with false like: -DskipTests=false is same.

With Surefire and Failsafe plugins and no additional configurations, tests can be run like shown( assuming that we have a multi-module project with my-service-api as a module and running maven commands for the api module from the root project) below:

Run all unit tests:
 ./mvnw -pl my-service-api clean test
Run specific unit test:
 ./mvnw -pl my-service-api clean test -Dtest=MyService1
Run specific unit test method:
 ./mvnw -pl my-service-api clean test -Dtest=MyService1#myServiceTest1

Run all integration tests:
 ./mvnw -pl my-service-api clean integration-test 
Run specific integration test:
 ./mvnw -pl my-service-api clean integration-test -Dit.test=MyServiceIT1
Run Run specific integration test method:
 ./mvnw -pl my-service-api clean integration-test -Dit.test=MyServiceIT1#method1

The problem with this is, running integration test, runs all unit-tests. If you DO NOT want unit tests to be run, when integration test/tests are run, we need to have a finer control to turn off unit tests.

The following explicit surefire and failsafe plugin configuration by three additional properties will give the finer control with the -DskipTests flag intact.

<properties> ... <!-- For finer control of running tests --> <skipTests>false</skipTests> <skipUTs>${skipTests}</skipUTs> <skipITs>${skipTests}</skipITs> </properties> <build> <plugins> ... <!-- surefire for unit tests --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>${maven-surefire-plugin.version}</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit47</artifactId> <version>${maven-surefire-plugin.version}</version> </dependency> </dependencies> <configuration> <skipTests>${skipUTs}</skipTests> </configuration> </plugin> <!-- failsafe for integration tests --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>${maven-failsafe-plugin.version}</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit47</artifactId> <version>${maven-surefire-plugin.version}</version> </dependency> </dependencies> <executions> <execution> <id>integration-tests</id> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> <configuration> <argLine>${integrationTestCoverageAgent}</argLine> <classesDirectory>${project.build.outputDirectory}</classesDirectory> <includes> <include>**/*IT.java</include> </includes> </configuration> </execution> </executions> <configuration> <skipTests>${skipITs}</skipTests> </configuration> </plugin> ... </plugins> ... </build>

With the above three defined additional properties and set to default: false for both the plugins, all tests run out of the box. Now, with their own separate properties defined for unit-tests and integration-tests, unit tests can be set not to run when we want to run specific integration test or all like:

Run specific integration test, don't run unit tests:
 ./mvnw -pl my-service-api clean integration-test -DskipUTs=true -Dit.test=MyServiceIT1

Run specific integration test, don't run unit tests (same as above, no explicit true):
 ./mvnw -pl my-service-api clean integration-test -DskipUTs -Dit.test=MyServiceIT1

But using verify goal instead of integration-test goal is better. Reason in TIP.

Run specific integration test (BETTER):
 ./mvnw -pl my-service-api clean verify -DskipUTs -Dit.test=MyServiceIT1

Run specific integration test (CLEANER):
 ./mvnw -pl my-service-api clean post-integration-test -DskipUTs -Dit.test=MyServiceIT1

But using post-integration-test goal instead of integration-test goal is cleaner. Reason in TIP.

TIP

Use verify goal instead of integration-test. If you have Docket container started for DB in integration test phase, it will not be stopped after finishing running tests. This will make your subsequent runs fail to start the container. You will running docker command to remove the container (docker rm -f <postgres-image-name>.

The goal: verify is a way to have cleaner way of executing integration test cases in this case.

The goal verify not only runs integration test case(s) but also verifies code coverage for coverage threshold check. The build might result with FAILURE at the end due to coverage threshold not being met. That is ok, we know that we are excluding unit tests and only running partial integration tests and won't expect coverage threshold to be met. But, it will execute cleanly stopping the containers if started any.

The goal: post-integration-test is another cleaner way to execute integration test cases.

Unlike the goal verify, this runs all phases up to post-integration-test (pre-integration-test, integration-test and post-integration-test) leaving out the last verify phase that checks code coverage.  This will execute cleanly stopping the containers if started.

A typical docker container plugin configurations to start before running integration test(s) and stop after running test(s) looks like:

<plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.33.0</version> <executions> <execution> <id>start</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <images> <image> <external> <type>properties</type> <prefix>postgres.docker</prefix> </external> </image> </images> </configuration> </plugin>


References