Skip to content

Commit 1bec70c

Browse files
jc65536Jason Chengtimtebeek
authored
Convert Testcontainers @Rule/@ClassRule to JUnit 5 @Container and add @Testcontainers (#838)
* Add recipe * Apply suggestions * Regenerate type table * Regenerate type table * Use `classpathFromResources` * Polish * Move recipe and include with v2 upgrade * Recreate type table --------- Co-authored-by: Jason Cheng <[email protected]> Co-authored-by: Tim te Beek <[email protected]>
1 parent 5f01caa commit 1bec70c

File tree

5 files changed

+463
-1
lines changed

5 files changed

+463
-1
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ recipeDependencies {
3737
parserClasspath("org.powermock:powermock-core:1.6.5")
3838
parserClasspath("org.springframework:spring-test:6.1.+")
3939
parserClasspath("org.testcontainers:testcontainers:1.20.6")
40+
parserClasspath("org.testcontainers:junit-jupiter:1.20.6")
4041
parserClasspath("org.testng:testng:7.+")
4142
parserClasspath("pl.pragmatists:JUnitParams:1.+")
4243
parserClasspath("uk.org.webcompere:system-stubs-core:2.1.8")
@@ -56,7 +57,6 @@ recipeDependencies {
5657
testParserClasspath("org.testcontainers:testcontainers-kafka:2.0.1")
5758
testParserClasspath("org.testcontainers:testcontainers-localstack:2.0.1")
5859
testParserClasspath("org.testcontainers:testcontainers-mysql:2.0.1")
59-
6060
}
6161

6262
val rewriteVersion = rewriteRecipe.rewriteVersion.get()
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.testing.testcontainers;
17+
18+
import org.openrewrite.ExecutionContext;
19+
import org.openrewrite.Preconditions;
20+
import org.openrewrite.Recipe;
21+
import org.openrewrite.TreeVisitor;
22+
import org.openrewrite.internal.ListUtils;
23+
import org.openrewrite.java.JavaIsoVisitor;
24+
import org.openrewrite.java.JavaParser;
25+
import org.openrewrite.java.JavaTemplate;
26+
import org.openrewrite.java.search.UsesType;
27+
import org.openrewrite.java.service.AnnotationService;
28+
import org.openrewrite.java.tree.J;
29+
import org.openrewrite.java.tree.TypeUtils;
30+
31+
import static java.util.Comparator.comparing;
32+
33+
/**
34+
* An OpenRewrite recipe that migrates JUnit 4 Testcontainers {@code @Rule} or {@code @ClassRule}
35+
* fields to JUnit 5's {@code @Container} and adds the {@code @Testcontainers} annotation to the
36+
* class if necessary.
37+
*/
38+
public class AddTestcontainersAnnotations extends Recipe {
39+
private static final String CLASS_RULE_FQN = "org.junit.ClassRule";
40+
private static final String RULE_FQN = "org.junit.Rule";
41+
private static final String GENERIC_CONTAINER_FQN = "org.testcontainers.containers.GenericContainer";
42+
private static final String TESTCONTAINERS_FQN = "org.testcontainers.junit.jupiter.Testcontainers";
43+
private static final String CONTAINER_FQN = "org.testcontainers.junit.jupiter.Container";
44+
45+
@Override
46+
public String getDisplayName() {
47+
return "Adopt `@Container` and add `@Testcontainers`";
48+
}
49+
50+
@Override
51+
public String getDescription() {
52+
return "Convert Testcontainers `@Rule`/`@ClassRule` to JUnit 5 `@Container` and add `@Testcontainers`.";
53+
}
54+
55+
@Override
56+
public TreeVisitor<?, ExecutionContext> getVisitor() {
57+
TreeVisitor<?, ExecutionContext> usesRule = Preconditions.or(
58+
new UsesType<>(RULE_FQN, true),
59+
new UsesType<>(CLASS_RULE_FQN, true)
60+
);
61+
return Preconditions.check(usesRule, new JavaIsoVisitor<ExecutionContext>() {
62+
@Override
63+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDeclaration, ExecutionContext ctx) {
64+
J.ClassDeclaration cd = super.visitClassDeclaration(classDeclaration, ctx);
65+
if (classDeclaration == cd) {
66+
return cd;
67+
}
68+
69+
maybeRemoveImport(RULE_FQN);
70+
maybeRemoveImport(CLASS_RULE_FQN);
71+
72+
if (service(AnnotationService.class).isAnnotatedWith(cd, TESTCONTAINERS_FQN)) {
73+
return cd;
74+
}
75+
76+
// Add class level annotation
77+
maybeAddImport(TESTCONTAINERS_FQN);
78+
return JavaTemplate.builder("@Testcontainers")
79+
.imports(TESTCONTAINERS_FQN)
80+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "testcontainers-1", "junit-jupiter-1"))
81+
.build()
82+
.apply(updateCursor(cd), cd.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
83+
}
84+
85+
@Override
86+
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations varDecls, ExecutionContext ctx) {
87+
if (!TypeUtils.isAssignableTo(GENERIC_CONTAINER_FQN, varDecls.getType())) {
88+
return varDecls;
89+
}
90+
if (!service(AnnotationService.class).isAnnotatedWith(varDecls, RULE_FQN) &&
91+
!service(AnnotationService.class).isAnnotatedWith(varDecls, CLASS_RULE_FQN)) {
92+
return varDecls;
93+
}
94+
95+
// Remove ClassRule/Rule annotations
96+
J.VariableDeclarations vd = varDecls.withLeadingAnnotations(ListUtils.filter(varDecls.getLeadingAnnotations(),
97+
ann -> !TypeUtils.isAssignableTo(RULE_FQN, ann.getType()) &&
98+
!TypeUtils.isAssignableTo(CLASS_RULE_FQN, ann.getType())));
99+
if (vd == varDecls || service(AnnotationService.class).isAnnotatedWith(varDecls, CONTAINER_FQN)) {
100+
return vd;
101+
}
102+
103+
// Add field level annotation
104+
maybeAddImport(CONTAINER_FQN);
105+
return JavaTemplate.builder("@Container")
106+
.imports(CONTAINER_FQN)
107+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "testcontainers-1", "junit-jupiter-1"))
108+
.build()
109+
.apply(updateCursor(vd), vd.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
110+
}
111+
});
112+
}
113+
114+
}
562 Bytes
Binary file not shown.

src/main/resources/META-INF/rewrite/testcontainers.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ name: org.openrewrite.java.testing.testcontainers.Testcontainers2Migration
2727
displayName: Migrate to testcontainers-java 2.x
2828
description: Change dependencies and types to migrate to testcontainers-java 2.x.
2929
recipeList:
30+
- org.openrewrite.java.testing.testcontainers.AddTestcontainersAnnotations
3031
- org.openrewrite.java.testing.testcontainers.ExplicitContainerImages
3132
- org.openrewrite.java.testing.testcontainers.GetHostMigration
3233
- org.openrewrite.java.ChangeType:

0 commit comments

Comments
 (0)