diff --git a/src/main/java/org/openrewrite/java/spring/test/SpringRulesToJUnitExtension.java b/src/main/java/org/openrewrite/java/spring/test/SpringRulesToJUnitExtension.java new file mode 100644 index 00000000..eff500cf --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/test/SpringRulesToJUnitExtension.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.spring.test; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.AnnotationMatcher; +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.tree.J; +import org.openrewrite.java.tree.TypeUtils; + +import static java.util.Comparator.comparing; + +public class SpringRulesToJUnitExtension extends Recipe { + + private static final String SPRING_CLASS_RULE = "org.springframework.test.context.junit4.rules.SpringClassRule"; + private static final String SPRING_METHOD_RULE = "org.springframework.test.context.junit4.rules.SpringMethodRule"; + private static final String EXTEND_WITH = "org.junit.jupiter.api.extension.ExtendWith"; + private static final String SPRING_EXTENSION = "org.springframework.test.context.junit.jupiter.SpringExtension"; + private static final AnnotationMatcher ANNOTATION_MATCHER = new AnnotationMatcher(String.format("@%s(%s.class)", EXTEND_WITH, SPRING_EXTENSION), true); + + + @Override + public String getDisplayName() { + return "Replace `SpringClassRule` and `SpringMethodRule` with JUnit 5 `SpringExtension`"; + } + + @Override + public String getDescription() { + return "Replace JUnit 4's `SpringClassRule` and `SpringMethodRule` with JUnit 5's `SpringExtension` or rely on an existing `@SpringBootTest`."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.or( + new UsesType<>(SPRING_CLASS_RULE, true), + new UsesType<>(SPRING_METHOD_RULE, true) + ), + new JavaIsoVisitor() { + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations vd = super.visitVariableDeclarations(multiVariable, ctx); + if (TypeUtils.isOfClassType(vd.getTypeAsFullyQualified(), SPRING_CLASS_RULE) || + TypeUtils.isOfClassType(vd.getTypeAsFullyQualified(), SPRING_METHOD_RULE)) { + maybeRemoveImport(SPRING_CLASS_RULE); + maybeRemoveImport(SPRING_METHOD_RULE); + maybeRemoveImport("org.junit.ClassRule"); + maybeRemoveImport("org.junit.Rule"); + + doAfterVisit(new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + if (cd.getLeadingAnnotations().stream().noneMatch(ANNOTATION_MATCHER::matches)) { + maybeAddImport(EXTEND_WITH); + maybeAddImport(SPRING_EXTENSION); + return JavaTemplate.builder("@ExtendWith(SpringExtension.class)") + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-jupiter-api", "spring-test")) + .imports(EXTEND_WITH, SPRING_EXTENSION) + .build() + .apply(getCursor(), cd.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); + + } + return cd; + } + }); + + //noinspection DataFlowIssue + return null; + } + return vd; + } + }); + } +} diff --git a/src/main/java/org/openrewrite/java/spring/test/package-info.java b/src/main/java/org/openrewrite/java/spring/test/package-info.java new file mode 100644 index 00000000..caea53e7 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/test/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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. + */ +@NonNullApi @NonNullFields +package org.openrewrite.java.spring.test; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; diff --git a/src/main/resources/META-INF/rewrite/spring-boot-24.yml b/src/main/resources/META-INF/rewrite/spring-boot-24.yml index e5576bc9..7eaff941 100644 --- a/src/main/resources/META-INF/rewrite/spring-boot-24.yml +++ b/src/main/resources/META-INF/rewrite/spring-boot-24.yml @@ -116,6 +116,7 @@ recipeList: - org.openrewrite.java.spring.boot2.UnnecessarySpringExtension - org.openrewrite.java.spring.boot2.ReplaceExtendWithAndContextConfiguration - org.openrewrite.java.spring.boot2.RemoveObsoleteSpringRunners + - org.openrewrite.java.spring.test.SpringRulesToJUnitExtension - org.openrewrite.java.dependencies.AddDependency: groupId: org.springframework.boot artifactId: spring-boot-tools diff --git a/src/test/java/org/openrewrite/java/spring/test/SpringRulesToJUnitExtensionTest.java b/src/test/java/org/openrewrite/java/spring/test/SpringRulesToJUnitExtensionTest.java new file mode 100644 index 00000000..294b50ec --- /dev/null +++ b/src/test/java/org/openrewrite/java/spring/test/SpringRulesToJUnitExtensionTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.spring.test; + +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 SpringRulesToJUnitExtensionTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new SpringRulesToJUnitExtension()) + .parser(JavaParser.fromJavaVersion() + .classpathFromResources(new InMemoryExecutionContext(), "spring-boot-test", "spring-test") + .dependsOn("package org.junit; public @interface ClassRule {}", "package org.junit; public @interface Rule {}") + ); + } + + @Test + @DocumentExample + void migrateWithSpringBootTestPresent() { + rewriteRun( + //language=java + java( + """ + import org.springframework.boot.test.context.SpringBootTest; + import org.springframework.test.context.junit4.rules.SpringClassRule; + import org.springframework.test.context.junit4.rules.SpringMethodRule; + import org.junit.ClassRule; + import org.junit.Rule; + + @SpringBootTest + class SomeTest { + + @ClassRule + public static final SpringClassRule springClassRule = new SpringClassRule(); + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + } + """, + """ + import org.springframework.boot.test.context.SpringBootTest; + + @SpringBootTest + class SomeTest { + + } + """ + ) + ); + } + + @Test + void migrateSingleAnnotation() { + rewriteRun( + //language=java + java( + """ + import org.springframework.boot.test.context.SpringBootTest; + import org.springframework.test.context.junit4.rules.SpringMethodRule; + import org.junit.Rule; + + @SpringBootTest + class SomeTest { + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + } + """, + """ + import org.springframework.boot.test.context.SpringBootTest; + + @SpringBootTest + class SomeTest { + + } + """ + ) + ); + } + + @Test + void migrateAndAddSpringExtension() { + rewriteRun( + //language=java + java( + """ + import org.springframework.test.context.junit4.rules.SpringClassRule; + import org.springframework.test.context.junit4.rules.SpringMethodRule; + import org.junit.ClassRule; + import org.junit.Rule; + + class SomeTest { + + @ClassRule + public static final SpringClassRule springClassRule = new SpringClassRule(); + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + } + """, + """ + import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.test.context.junit.jupiter.SpringExtension; + + @ExtendWith(SpringExtension.class) + class SomeTest { + + } + """ + ) + ); + } +}