Skip to content

Commit 01a8a88

Browse files
authored
Add recipe for Sonar RSPEC-2699 Tests should have assertions. (#123)
* Add best-practices recipe for Sonar RSPEC-2699 Tests should have assertions.
1 parent 82bf81a commit 01a8a88

File tree

4 files changed

+403
-0
lines changed

4 files changed

+403
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (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://www.apache.org/licenses/LICENSE-2.0
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.cleanup;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.*;
21+
import org.openrewrite.java.AnnotationMatcher;
22+
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.JavaParser;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.Statement;
26+
27+
import java.util.Arrays;
28+
import java.util.List;
29+
30+
/**
31+
* For Tests not having any assertions, wrap the statements with JUnit 5's Assertions.assertThrowDoesNotThrow.
32+
*
33+
* <a href="https://rules.sonarsource.com/java/tag/tests/RSPEC-2699">Sonar Source RSPEC-2699</a>
34+
*/
35+
@Incubating(since = "1.2.0")
36+
@Value
37+
@EqualsAndHashCode(callSuper = true)
38+
public class TestsShouldIncludeAssertions extends Recipe {
39+
40+
private static final AnnotationMatcher JUNIT_JUPITER_TEST = new AnnotationMatcher("@org.junit.jupiter.api.Test");
41+
42+
private static final String THROWING_SUPPLIER_FQN = "org.junit.jupiter.api.function.ThrowingSupplier";
43+
private static final String ASSERTIONS_FQN = "org.junit.jupiter.api.Assertions";
44+
private static final String ASSERTIONS_DOES_NOT_THROW_FQN = "org.junit.jupiter.api.Assertions.assertDoesNotThrow";
45+
private static final String ASSERT_DOES_NOT_THROW = "assertDoesNotThrow";
46+
47+
private static final ThreadLocal<JavaParser> ASSERTIONS_PARSER = ThreadLocal.withInitial(() ->
48+
JavaParser.fromJavaVersion().dependsOn(Arrays.asList(
49+
Parser.Input.fromString(
50+
"package org.junit.jupiter.api.function;" +
51+
"public interface ThrowingSupplier<T> {T get() throws Throwable;}"
52+
),
53+
Parser.Input.fromString(
54+
"package org.junit.jupiter.api;" +
55+
"import org.junit.jupiter.api.function.ThrowingSupplier;" +
56+
"class AssertDoesNotThrow {" +
57+
" static <T> T assertDoesNotThrow(ThrowingSupplier<T> supplier) {\n" +
58+
" return (T)(Object)null;\n" +
59+
" }" +
60+
"}"
61+
)
62+
)).build());
63+
64+
@Option(displayName = "Assertions",
65+
description = "List of fully qualified classes and or methods used for identifying assertion statements.",
66+
example = "org.junit.jupiter.api.Assertions, org.hamcrest.MatcherAssert, io.vertx.ext.unit.TestContext.verify")
67+
List<String> assertions;
68+
69+
@Override
70+
public String getDisplayName() {
71+
return "Tests should include assertions";
72+
}
73+
74+
@Override
75+
public String getDescription() {
76+
return "For Tests not having any assertions, wrap the statements with JUnit 5's Assertions.assertThrowDoesNotThrow (Sonar RSPEC-2699).";
77+
}
78+
79+
@Override
80+
protected TreeVisitor<?, ExecutionContext> getVisitor() {
81+
return new TestIncludesAssertionsVisitor();
82+
}
83+
84+
private class TestIncludesAssertionsVisitor extends JavaIsoVisitor<ExecutionContext> {
85+
86+
@Override
87+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) {
88+
if ((!methodIsTest(method) || method.getBody() == null)
89+
|| methodHasAssertion(method.getBody().getStatements())) {
90+
return method;
91+
}
92+
93+
J.MethodDeclaration md = super.visitMethodDeclaration(method, executionContext);
94+
J.Block body = md.getBody();
95+
if (body != null) {
96+
StringBuilder t = new StringBuilder("{\nassertDoesNotThrow(() -> {");
97+
body.getStatements().forEach(st -> t.append(st.print()).append(";"));
98+
t.append("});\n}");
99+
100+
body = body.withTemplate(template(t.toString())
101+
.imports(THROWING_SUPPLIER_FQN)
102+
.staticImports(ASSERTIONS_DOES_NOT_THROW_FQN)
103+
.javaParser(ASSERTIONS_PARSER.get()).build(),
104+
body.getCoordinates().replace());
105+
md = maybeAutoFormat(md, md.withBody(body), executionContext, getCursor().dropParentUntil(J.class::isInstance));
106+
maybeAddImport(ASSERTIONS_FQN, ASSERT_DOES_NOT_THROW);
107+
}
108+
return md;
109+
}
110+
111+
private boolean methodIsTest(J.MethodDeclaration methodDeclaration) {
112+
return methodDeclaration.getLeadingAnnotations().stream()
113+
.filter(annotation -> JUNIT_JUPITER_TEST.matches(annotation))
114+
.findAny().isPresent();
115+
}
116+
117+
private boolean methodHasAssertion(List<Statement> statements) {
118+
return statements.stream()
119+
.filter(J.MethodInvocation.class::isInstance)
120+
.map(J.MethodInvocation.class::cast)
121+
.filter(this::isAssertion).findAny().isPresent();
122+
}
123+
124+
private boolean isAssertion(J.MethodInvocation methodInvocation) {
125+
if (methodInvocation.getType() == null) {
126+
return false;
127+
}
128+
String fqt = methodInvocation.getType().getDeclaringType().getFullyQualifiedName();
129+
for (String assertionClassOrPackage : assertions) {
130+
if (fqt.startsWith(assertionClassOrPackage)) {
131+
return true;
132+
}
133+
}
134+
135+
if (methodInvocation.getSelect() != null && methodInvocation.getSelect() instanceof J.MethodInvocation
136+
&& ((J.MethodInvocation) methodInvocation.getSelect()).getType() != null) {
137+
J.MethodInvocation selectMethod = (J.MethodInvocation) methodInvocation.getSelect();
138+
if (selectMethod.getType() != null) {
139+
String select = selectMethod.getType().getDeclaringType().getFullyQualifiedName() + "." + selectMethod.getSimpleName();
140+
for (String assertMethod : assertions) {
141+
if (select.equals(assertMethod)) {
142+
return true;
143+
}
144+
}
145+
}
146+
}
147+
return false;
148+
}
149+
}
150+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (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://www.apache.org/licenses/LICENSE-2.0
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+
@NonNullApi
17+
package org.openrewrite.java.testing.cleanup;
18+
19+
import org.openrewrite.internal.lang.NonNullApi;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#
2+
# Copyright 2020 the original author or authors.
3+
# <p>
4+
# Licensed under the Apache License, Version 2.0 (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://www.apache.org/licenses/LICENSE-2.0
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+
---
17+
type: specs.openrewrite.org/v1beta/recipe
18+
name: org.openrewrite.java.testing.cleanup.BestPractices
19+
displayName: Testing Frameworks best practices
20+
description: Applies best practices to tests.
21+
tags:
22+
- sonar
23+
- testing
24+
recipeList:
25+
- org.openrewrite.java.testing.cleanup.TestsShouldIncludeAssertions:
26+
assertions:
27+
- org.assertj.core.api
28+
- org.junit.jupiter.api.Assertions
29+
- org.hamcrest.MatcherAssert
30+
- org.mockito.Mockito.verify
31+
- org.springframework.test.web.servlet.ResultActions.andExpect

0 commit comments

Comments
 (0)