Old Java versions
Java 13 introduced text blocks (multi-line strings) as a preview feature, finalized in Java 15. They make working with multi-line strings much more readable, but traditional string concatenation is still commonly found in older tests.
String concatenation
Traditional string concatenation with +
operators for multi-line strings is hard to read and maintain.
Each line needs explicit \n
newline characters and quote escaping.
- Before
- After
import com.github.timtebeek.books.Bundle;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TextBlockTest {
@Test
void summary() {
String summary = new Bundle().summary();
assertEquals("Books:\n" +
"Effective Java by Joshua Bloch (2001)\n" +
"Java Concurrency in Practice by Brian Goetz (2006)\n" +
"Clean Code by Robert C. Martin (2008)\n" +
"Authors:\n" +
"Joshua Bloch\n" +
"Brian Goetz\n" +
"Robert C. Martin\n" +
"Total books: 3\n" +
"Total authors: 3\n",
summary);
}
}
String concatenation with +
is verbose and error-prone. Missing newlines, extra spaces, and quote escaping make it hard to see the actual content.
import com.github.timtebeek.books.Bundle;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TextBlockTest {
@Test
void summary() {
String summary = new Bundle().summary();
assertEquals("""
Books:
Effective Java by Joshua Bloch (2001)
Java Concurrency in Practice by Brian Goetz (2006)
Clean Code by Robert C. Martin (2008)
Authors:
Joshua Bloch
Brian Goetz
Robert C. Martin
Total books: 3
Total authors: 3
""",
summary);
}
}
Text blocks make multi-line strings much more readable. The content is exactly as it appears, without escape sequences or concatenation operators.
Even better with AssertJ
While text blocks improve readability, AssertJ can make multi-line string comparisons even clearer with better diff output.
- Before
- After
import com.github.timtebeek.books.Bundle;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TextBlockTest {
@Test
void summary() {
String summary = new Bundle().summary();
assertEquals("""
Books:
Effective Java by Joshua Bloch (2001)
Java Concurrency in Practice by Brian Goetz (2006)
Clean Code by Robert C. Martin (2008)
Authors:
Joshua Bloch
Brian Goetz
Robert C. Martin
Total books: 3
Total authors: 3
""",
summary);
}
}
JUnit's assertEquals()
works with text blocks, but the diff output for multi-line strings can be hard to read when tests fail.
import com.github.timtebeek.books.Bundle;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TextBlockTest {
@Test
void summary() {
String summary = new Bundle().summary();
assertThat(summary).isEqualToIgnoringWhitespace("""
Books:
Effective Java by Joshua Bloch (2001)
Java Concurrency in Practice by Brian Goetz (2006)
Clean Code by Robert C. Martin (2008)
Authors:
Joshua Bloch
Brian Goetz
Robert C. Martin
Total books: 3
Total authors: 3
""");
}
}
AssertJ provides better diff output for multi-line strings and offers flexible comparison methods like isEqualToIgnoringWhitespace()
for when exact whitespace doesn't matter.
Automated migration
The OpenRewrite UpgradeToJava21 recipe can automatically convert string concatenation to text blocks in your Java code, as well as upgrade other parts of your application to align with Java 21 best practices, like using getFirst()
and getLast()
on sequenced collections.
In this case we're going to look at upgrading just the tests to Java 21; not yet upgrading the main source code.
This split in the Java version used for src/main
and src/test
is possible with both Maven and Gradle.
With Maven, you can set the maven.compiler.testRelease
property to 21 in the maven-compiler-plugin
configuration.
We find starting out with newer Java versions in tests only is often a good way to start adoption. Developers can start writing new tests and updating existing tests using the latest Java features, while the main application code can be upgraded at a more leisurely pace as it continues to target the version you're on. You'll be able to adapt your build pipelines already, and prove to management that the newer Java version work well for your team.
We will first create a custom recipe file in the root of your project, that applies the UpgradeToJava21
recipe only to test code,
by using a dedicated precondition that matches test code only.
---
type: specs.openrewrite.org/v1beta/recipe
name: com.github.timtebeek.Java21ForTests
displayName: Adopt Java 21 for tests
description: Upgrade your tests to Java 21.
preconditions:
- org.openrewrite.java.search.IsLikelyTest
recipeList:
- org.openrewrite.java.migrate.UpgradeToJava21
You can run OpenRewrite recipes directly from IntelliJ IDEA Ultimate; after adding the file to your repository, you should see a run icon in the left margin offering to run the recipe.
If you're not using IntelliJ IDEA Ultimate, you can run the above recipe using one of the following methods.
- Moderne CLI
- Maven Command Line
- Maven POM
- Gradle init script
- Gradle
The Moderne CLI allows you to run OpenRewrite recipes on your project without needing to modify your build files, against serialized Lossless Semantic Tree (LST) of your project for a considerable performance boost & across projects.
You will need to have configured the Moderne CLI on your machine before you can run the following command.
- If project serialized Lossless Semantic Tree is not yet available locally, then build the LST. This is only needed the first time, or after extensive changes:
mod build ~/workspace/
- If the recipe is not available locally yet, then you can install it once using:
mod config recipes jar install org.openrewrite.recipe:rewrite-migrate-java:LATEST
- Run the recipe.
mod run ~/workspace/ --recipe com.github.timtebeek.Java21ForTests
You will need to have Maven installed on your machine before you can run the following command.
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE -Drewrite.activeRecipes=com.github.timtebeek.Java21ForTests -Drewrite.exportDatatables=true
You may add the plugin to your pom.xml
file, so that it is available for all developers and CI/CD pipelines.
- Add the following to your
pom.xml
file:
<project>
<build>
<plugins>
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>LATEST</version>
<configuration>
<exportDatatables>true</exportDatatables>
<activeRecipes>
<recipe>com.github.timtebeek.Java21ForTests</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-migrate-java</artifactId>
<version>LATEST</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
- Run run the recipe.
mvn rewrite:run
Gradle init scripts are a good way to try out a recipe without modifying your build.gradle
file.
- Create a file named
init.gradle
in the root of your project.
initscript {
repositories {
maven { url "https://plugins.gradle.org/m2" }
}
dependencies { classpath("org.openrewrite:plugin:latest.release") }
}
rootProject {
plugins.apply(org.openrewrite.gradle.RewritePlugin)
dependencies {
rewrite("org.openrewrite.recipe:rewrite-migrate-java:latest.release")
}
rewrite {
activeRecipe("com.github.timtebeek.Java21ForTests")
setExportDatatables(true)
}
afterEvaluate {
if (repositories.isEmpty()) {
repositories {
mavenCentral()
}
}
}
}
- Run the recipe.
gradle --init-script init.gradle rewriteRun
You can add the plugin to your build.gradle
file, so that it is available for all developers and CI/CD pipelines.
- Add the following to your
build.gradle
file:
plugins {
id("org.openrewrite.rewrite") version("latest.release")
}
rewrite {
activeRecipe("com.github.timtebeek.Java21ForTests")
setExportDatatables(true)
}
repositories {
mavenCentral()
}
dependencies {
rewrite("org.openrewrite.recipe:rewrite-migrate-java:latest.release")
}
- Run
gradle rewriteRun
to run the recipe.