Spring Boot, Java 16+, JUnit4, JUnit5 & PowerMockito Salad

Pasindu Senanayake
ParallaxTec
Published in
4 min readJun 21, 2022
Food top view photo created by timolina — www.freepik.com

Java is evolving and it’s evolving fast. It’s not just evolving but it’s becoming better faster and more secure. But the million-dollar question is what’s the status of the Java Eco-System? Is it evolving as fast as Java? Unfortunately not. This slow-moving Eco-System has become a massive problem for developers when working with the latest versions of Java and I’m going to make your life easy by solving one of those problems.

As the topic suggests with fast-evolving Java and slow-moving libraries and packages we are actually in Salad. If your application using Powermockito for testing and trying to move to Java 17 with Junit5, my friend you are asking for trouble. But don’t worry with a couple of workarounds we can still make this work.

First things first let’s see what are the main blockers we are facing.

1. Java Strong Encapsulation of JDK Internals

This is the latest change that is added to the Java 17 spec and it breaks most of the reflection capabilities. When reflection is broke Powermockito becomes helpless. So it’s required to manually open java internals for Powermockito. How can we do that? let’s see that in a couple of minutes.

2. Mockito API deprecation and removal

Powermockito is pretty old and it uses Mockito APIs internally. In order to support JUnit5 Mockito has been updated (version 4.x) and on its journey, it has removed multiple old methods. So if your Powermock version, Mockito Version and JUnit5 are not compatible with each other you wouldn’t be able to execute a single test.

3. Powermockito Runner is not Compatible with JUnit5

When the library isn’t updated to support the main functionality do you think it would support for extensions?! So JUnit5 and Powermockito are a big NO-NO. In that case, we have to stick with JUnit4.

Enough with problems! let’s see what the solution is

First of all, let’s separate the concerns

  1. Unit tests without Powermockito can be extended with JUnit5 runner and can be written as follows.
  2. Unit tests with Powermockito would require the legacy JUnit4 runner and can be written as follows.

Now it’s time to set the maven environment to run both JUnit 4 and 5. Maven SureFire is the key to that. Even though the document says just adding maven surefire would do the trick it won’t. The platform is need to be set and individual executors need to be provided with the maven surefire plugin as follows.

<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
@{argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
</argLine>
</configuration>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit4</artifactId>
<version>2.22.2</version>
</dependency>
</dependencies>
</plugin>

While junit-platform-surefire-provider provides the platform for the execution junit-jupiter-engine is responsible for the JUnit5 execution and surefire-junit4 is responsible for the JUnit4 execution.

Now we have configured the key areas. If you look at the above maven pom carefully you would see a <argLine> with multiple commands.

<argLine>
@{argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
</argLine>

Let’s keep the @{argLine} aside for a bit. All other 3 command-line arguments are there to open up JDK internals. Please keep in mind opening up internals is not recommended for application usage but acceptable for unit tests. Even with unit tests make sure to open the Internals only if it’s absolutely required. All the possible Java openings are listed here.

With all the above configurations we have solved most of our problems except the Mockito version mismatch. In order to solve that issue we have to downgrade Mockito from version 4.x to 3.x (the latest versions of version 3) as follows.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>

Since we use Spring Boot, spring-boot-starter-test dependency contains mockito-core 4.x. So we don’t have an option other than removing the internal dependency and injecting our own mockito-core 3.12.4.

All right, are we good? Yes but one small problem remains. That’s the unit test coverage. With all these plugins and versions will the coverage works? Well, Jacoco is a very powerful tool but it requires a little modification to perform its magic. And that modification is the @{argLine} keyword in the <argLine> command. So what’s going on there? When the coverage reports are generated behind the scenes Jacoco adds a couple of arguments to the maven surface plugin. When we don’t provide any custom arguments Jacoco is perfectly capable of doing it on his own. But when we add custom arguments with <argLine> Jacoco needs our help. By adding @{argLine} keyword we simply help Jacoco to insert those arguments to get the coverage reports. For the sake of completion, I have added the Jacoco library and plugin versions as well.

<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
</dependency>
----------------------------------------------------------<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<configuration>
<excludes>
<exclude>**/AppStartupRunner.java</exclude>
<exclude>*</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>default-instrument</id>
<goals>
<goal>instrument</goal>
</goals>
</execution>
<execution>
<id>default-restore-instrumented-classes</id>
<goals>
<goal>restore-instrumented-classes</goal>
</goals>
</execution>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>

Keep in mind mockito-inline can’t work with the above setup. There is no version of mockito-inline that can work with both PowerMockito and JUnit5. So unfortunately we can’t move forward with that combination. But luckily if you are using PowerMockito there is no need for mockito-inline because everything we do with mockito-inline can be done with PowerMockito as herewell.

That’s it about the salad of JUnit testing friends. You can find the full repository here.

Keep coding !!!

--

--

Pasindu Senanayake
ParallaxTec

Graduate of Department of Computer Science and Engineering at University of Moratuwa