diff --git a/build.gradle.kts b/build.gradle.kts index e82431a7d..d3fc8b758 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,7 @@ recipeDependencies { parserClasspath("org.powermock:powermock-core:1.6.5") parserClasspath("org.springframework:spring-test:6.1.+") parserClasspath("org.testcontainers:testcontainers:1.20.6") + parserClasspath("org.testcontainers:junit-jupiter:1.20.6") parserClasspath("org.testng:testng:7.+") parserClasspath("pl.pragmatists:JUnitParams:1.+") parserClasspath("uk.org.webcompere:system-stubs-core:2.1.8") @@ -56,7 +57,6 @@ recipeDependencies { testParserClasspath("org.testcontainers:testcontainers-kafka:2.0.1") testParserClasspath("org.testcontainers:testcontainers-localstack:2.0.1") testParserClasspath("org.testcontainers:testcontainers-mysql:2.0.1") - } val rewriteVersion = rewriteRecipe.rewriteVersion.get() diff --git a/src/main/java/org/openrewrite/java/testing/testcontainers/AddTestcontainersAnnotations.java b/src/main/java/org/openrewrite/java/testing/testcontainers/AddTestcontainersAnnotations.java new file mode 100644 index 000000000..702cc8e07 --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/testcontainers/AddTestcontainersAnnotations.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 the original author or authors. + *
+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * https://docs.moderne.io/licensing/moderne-source-available-license + *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.testing.testcontainers;
+
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Preconditions;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.search.UsesType;
+import org.openrewrite.java.service.AnnotationService;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.TypeUtils;
+
+import static java.util.Comparator.comparing;
+
+/**
+ * An OpenRewrite recipe that migrates JUnit 4 Testcontainers {@code @Rule} or {@code @ClassRule}
+ * fields to JUnit 5's {@code @Container} and adds the {@code @Testcontainers} annotation to the
+ * class if necessary.
+ */
+public class AddTestcontainersAnnotations extends Recipe {
+ private static final String CLASS_RULE_FQN = "org.junit.ClassRule";
+ private static final String RULE_FQN = "org.junit.Rule";
+ private static final String GENERIC_CONTAINER_FQN = "org.testcontainers.containers.GenericContainer";
+ private static final String TESTCONTAINERS_FQN = "org.testcontainers.junit.jupiter.Testcontainers";
+ private static final String CONTAINER_FQN = "org.testcontainers.junit.jupiter.Container";
+
+ @Override
+ public String getDisplayName() {
+ return "Adopt `@Container` and add `@Testcontainers`";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Convert Testcontainers `@Rule`/`@ClassRule` to JUnit 5 `@Container` and add `@Testcontainers`.";
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ TreeVisitor, ExecutionContext> usesRule = Preconditions.or(
+ new UsesType<>(RULE_FQN, true),
+ new UsesType<>(CLASS_RULE_FQN, true)
+ );
+ return Preconditions.check(usesRule, new JavaIsoVisitor
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.testing.testcontainers;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.InMemoryExecutionContext;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+class AddTestcontainersAnnotationsTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new AddTestcontainersAnnotations())
+ .parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(),
+ "junit-4", "testcontainers-1", "junit-jupiter-1"));
+ }
+
+ @DocumentExample
+ @Test
+ void convertsSingleGenericContainerRule() {
+ rewriteRun(
+ // language=java
+ java(
+ // before
+ """
+ import org.junit.Rule;
+ import org.junit.Test;
+ import org.testcontainers.containers.GenericContainer;
+
+ class MyTest {
+ @Rule
+ public GenericContainer> myContainer = new GenericContainer<>("redis:latest");
+ }
+ """,
+ // after
+ """
+ import org.junit.Test;
+ import org.testcontainers.containers.GenericContainer;
+ import org.testcontainers.junit.jupiter.Container;
+ import org.testcontainers.junit.jupiter.Testcontainers;
+
+ @Testcontainers
+ class MyTest {
+ @Container
+ public GenericContainer> myContainer = new GenericContainer<>("redis:latest");
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void convertsMultipleContainerRules() {
+ rewriteRun(
+ // language=java
+ java(
+ // before
+ """
+ import org.junit.Rule;
+ import org.testcontainers.containers.GenericContainer;
+
+ class MyTest {
+ @Rule
+ public GenericContainer> redis = new GenericContainer<>("redis:latest");
+
+ @Rule
+ public GenericContainer> postgres = new GenericContainer<>("postgres:latest");
+ }
+ """,
+ // after
+ """
+ import org.testcontainers.containers.GenericContainer;
+ import org.testcontainers.junit.jupiter.Container;
+ import org.testcontainers.junit.jupiter.Testcontainers;
+
+ @Testcontainers
+ class MyTest {
+ @Container
+ public GenericContainer> redis = new GenericContainer<>("redis:latest");
+
+ @Container
+ public GenericContainer> postgres = new GenericContainer<>("postgres:latest");
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void convertsMixedRuleAndClassRule() {
+ rewriteRun(
+ // language=java
+ java(
+ // before
+ """
+ import org.junit.ClassRule;
+ import org.junit.Rule;
+ import org.testcontainers.containers.GenericContainer;
+
+ class MyTest {
+ @ClassRule
+ public static GenericContainer> redis = new GenericContainer<>("redis:latest");
+
+ @Rule
+ public GenericContainer> postgres = new GenericContainer<>("postgres:latest");
+ }
+ """,
+ // after
+ """
+ import org.testcontainers.containers.GenericContainer;
+ import org.testcontainers.junit.jupiter.Container;
+ import org.testcontainers.junit.jupiter.Testcontainers;
+
+ @Testcontainers
+ class MyTest {
+ @Container
+ public static GenericContainer> redis = new GenericContainer<>("redis:latest");
+
+ @Container
+ public GenericContainer> postgres = new GenericContainer<>("postgres:latest");
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void convertsSubclassedContainerRule() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ package com.uber.fievel.testing.redis;
+
+ import org.testcontainers.containers.GenericContainer;
+
+ public class UberRedisContainer