From d73c3905241a7edc4f1a953f6f1c2163c6b33d45 Mon Sep 17 00:00:00 2001 From: Michael Keppler Date: Wed, 24 Apr 2024 11:39:04 +0200 Subject: [PATCH] Recipe to make tests throw at most a single Exception (#510) * new recipe to simplify test method throws declarations * Minor polish * Update SimplifyTestThrows.java --------- Co-authored-by: Tim te Beek --- .../testing/cleanup/SimplifyTestThrows.java | 116 +++++++++++++++++ .../cleanup/SimplifyTestThrowsTest.java | 118 ++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrows.java create mode 100644 src/test/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrowsTest.java diff --git a/src/main/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrows.java b/src/main/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrows.java new file mode 100644 index 000000000..2c97e26bf --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrows.java @@ -0,0 +1,116 @@ +/* + * 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.testing.cleanup; + +import org.openrewrite.*; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; + +import java.time.Duration; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +public class SimplifyTestThrows extends Recipe { + + private static final String FQN_JAVA_LANG_EXCEPTION = "java.lang.Exception"; + + @Override + public String getDisplayName() { + return "Simplify `throws` statements of tests"; + } + + @Override + public String getDescription() { + return "Replace all thrown exception classes of test method signatures by `Exception`."; + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(1); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.or( + new UsesType<>("org.junit.jupiter.api.Test", false), + new UsesType<>("org.junit.jupiter.api.TestTemplate", false), + new UsesType<>("org.junit.jupiter.api.RepeatedTest", false), + new UsesType<>("org.junit.jupiter.params.ParameterizedTest", false), + new UsesType<>("org.junit.jupiter.api.TestFactory", false) + ), + new JavaIsoVisitor() { + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration m = super.visitMethodDeclaration(method, ctx); + + // reject invalid methods + if (TypeUtils.isOverride(m.getMethodType()) + || !hasJUnit5MethodAnnotation(method) + || throwsNothingOrException(method)) { + return m; + } + + // remove imports of the old exceptions + for (NameTree t : m.getThrows()) { + JavaType.FullyQualified type = TypeUtils.asFullyQualified(t.getType()); + if (type != null) { + maybeRemoveImport(type); + } + } + + // overwrite the throws declarations + J.Identifier exceptionIdentifier = new J.Identifier(Tree.randomId(), + Space.SINGLE_SPACE, + Markers.EMPTY, + emptyList(), + "Exception", + JavaType.ShallowClass.build(FQN_JAVA_LANG_EXCEPTION), + null); + return m.withThrows(singletonList(exceptionIdentifier)); + } + + /** + * @return true if the method has no throws clause or only throws Exception + */ + private boolean throwsNothingOrException(J.MethodDeclaration method) { + @Nullable List th = method.getThrows(); + if (th == null || th.isEmpty()) { + return true; + } + return th.size() == 1 && TypeUtils.isOfClassType(th.get(0).getType(), FQN_JAVA_LANG_EXCEPTION); + } + + private boolean hasJUnit5MethodAnnotation(J.MethodDeclaration method) { + for (J.Annotation a : method.getLeadingAnnotations()) { + if (TypeUtils.isOfClassType(a.getType(), "org.junit.jupiter.api.Test") + || TypeUtils.isOfClassType(a.getType(), "org.junit.jupiter.api.TestTemplate") + || TypeUtils.isOfClassType(a.getType(), "org.junit.jupiter.api.RepeatedTest") + || TypeUtils.isOfClassType(a.getType(), "org.junit.jupiter.params.ParameterizedTest") + || TypeUtils.isOfClassType(a.getType(), "org.junit.jupiter.api.TestFactory")) { + return true; + } + } + return false; + } + }); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrowsTest.java b/src/test/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrowsTest.java new file mode 100644 index 000000000..8e7bf84b7 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/cleanup/SimplifyTestThrowsTest.java @@ -0,0 +1,118 @@ +/* + * 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.testing.cleanup; + +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 SimplifyTestThrowsTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(), + "junit-jupiter-api-5.9", "junit-jupiter-params-5.9")) + .recipe(new SimplifyTestThrows()); + } + + @DocumentExample + @Test + void simplifyExceptions() { + //language=java + rewriteRun( + java( + """ + import java.io.IOException; + import org.junit.jupiter.api.Test; + + class ATest { + @Test + void throwsMultiple() throws IOException, ArrayIndexOutOfBoundsException, ClassCastException { + } + } + """, + """ + import org.junit.jupiter.api.Test; + + class ATest { + @Test + void throwsMultiple() throws Exception { + } + } + """ + ) + ); + } + + @Test + void normalMethodNoChanges() { + //language=java + rewriteRun( + java( + """ + import java.io.IOException; + import org.junit.jupiter.api.Test; + + class ATest { + void noTest() throws IOException { + } + } + """ + ) + ); + } + + @Test + void noThrowsDeclaration() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + class ATest { + @Test + void noThrows() { + } + } + """ + ) + ); + } + + @Test + void usesGeneralExceptionAlready() { + //language=java + rewriteRun( + java( + """ + import org.junit.jupiter.api.Test; + + class ATest { + @Test + void throwsEx() throws Exception { + } + } + """ + ) + ); + } +}