Skip to content

Commit 67d0fdd

Browse files
SONARJAVA-5480 S2699 recognizes SpringBoot AssertableApplicationContext (#5125)
1 parent da161d2 commit 67d0fdd

5 files changed

Lines changed: 181 additions & 5 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"ruleKey": "S2699",
33
"hasTruePositives": true,
4-
"falseNegatives": 152,
4+
"falseNegatives": 154,
55
"falsePositives": 1
66
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package checks.tests.AssertionsInTestsCheck;
2+
3+
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
4+
import org.springframework.boot.test.context.runner.ContextConsumer;
5+
6+
public class ForeignNonAssertingContextConsumer implements ContextConsumer<AssertableApplicationContext> {
7+
@Override
8+
public void accept(AssertableApplicationContext context) throws Throwable {
9+
// do something but no assertions
10+
}
11+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package checks.tests.AssertionsInTestsCheck;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
5+
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
6+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
7+
import org.springframework.boot.test.context.runner.ContextConsumer;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
class SpringBootAppContextRunnerSampleTest {
12+
13+
ApplicationContextRunner contextRunner = new ApplicationContextRunner();
14+
15+
@Test
16+
void test_with_assertable_app_context_different_file() { // FN, contextConsumer has no assertions, but we cannot know
17+
var contextConsumer = new ForeignNonAssertingContextConsumer();
18+
contextRunner.run(contextConsumer);
19+
}
20+
21+
@Test
22+
void test_with_assertable_app_context() { // Compliant, contextConsumer has param type AssertableApplicationContext
23+
contextRunner.run(getAssertingContextConsumer());
24+
}
25+
26+
@Test
27+
void test_with_assertable_app_context2() { // Compliant, contextConsumer has param type AssertableApplicationContext
28+
var contextConsumer = new LocalAssertingContextConsumer();
29+
contextRunner.run(contextConsumer);
30+
}
31+
32+
@Test
33+
void test_without_assertable_app_context() { // Noncompliant
34+
var nonAssertingContextConsumer = new NonAssertingContextConsumer();
35+
contextRunner.run(nonAssertingContextConsumer);
36+
}
37+
38+
@Test
39+
void test_with_lambda_assertable_app_ctx() {
40+
contextRunner.run(context -> {
41+
assertThat(context).getBean("myBean").isNotNull();
42+
});
43+
}
44+
45+
@Test
46+
void test_without_assertable_app_ctx() { // Noncompliant
47+
contextRunner.run((ContextConsumer<ApplicationContextAssertProvider>) _ -> {
48+
// do some unrelated stuff ...
49+
});
50+
}
51+
52+
private LocalAssertingContextConsumer getAssertingContextConsumer(){
53+
return new LocalAssertingContextConsumer();
54+
}
55+
56+
class LocalAssertingContextConsumer implements ContextConsumer<AssertableApplicationContext> {
57+
@Override
58+
public void accept(AssertableApplicationContext context) throws Throwable {
59+
assertThat(context).getBean("myBean").isNotNull();
60+
}
61+
}
62+
63+
class NonAssertingContextConsumer implements ContextConsumer<AssertableApplicationContext> {
64+
private int count = 0;
65+
66+
@Override
67+
public void accept(AssertableApplicationContext context) throws Throwable {
68+
count++;
69+
}
70+
}
71+
72+
}

java-checks/src/main/java/org/sonar/java/checks/tests/AssertionsInTestsCheck.java

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
import org.sonar.check.RuleProperty;
2828
import org.sonar.java.checks.helpers.AbstractAssertionVisitor;
2929
import org.sonar.java.model.ModifiersUtils;
30+
import org.sonar.java.model.declaration.ClassTreeImpl;
31+
import org.sonar.java.model.expression.MethodInvocationTreeImpl;
3032
import org.sonar.plugins.java.api.JavaFileScanner;
3133
import org.sonar.plugins.java.api.JavaFileScannerContext;
3234
import org.sonar.plugins.java.api.semantic.MethodMatchers;
3335
import org.sonar.plugins.java.api.semantic.Symbol;
3436
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
37+
import org.sonar.plugins.java.api.semantic.Type;
3538
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
3639
import org.sonar.plugins.java.api.tree.ClassTree;
40+
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
3741
import org.sonar.plugins.java.api.tree.MethodTree;
3842
import org.sonar.plugins.java.api.tree.Modifier;
3943
import org.sonar.plugins.java.api.tree.Tree;
@@ -54,6 +58,13 @@ public class AssertionsInTestsCheck extends BaseTreeVisitor implements JavaFileS
5458
public String customAssertionMethods = "";
5559
private MethodMatchers customAssertionMethodsMatcher = null;
5660

61+
private static final MethodMatchers SPRING_BOOT_APP_CTX_RUNNER_RUN_MATCHER = MethodMatchers.create()
62+
.ofTypes("org.springframework.boot.test.context.runner.ApplicationContextRunner")
63+
.names("run")
64+
.addParametersMatcher("org.springframework.boot.test.context.runner.ContextConsumer")
65+
.build();
66+
private static final MethodInvocationMatcherVisitor SPRING_BOOT_APP_CTX_RUNNER_VISITOR = new MethodInvocationMatcherVisitor(SPRING_BOOT_APP_CTX_RUNNER_RUN_MATCHER);
67+
5768
private final Map<Symbol, Boolean> assertionInMethod = new HashMap<>();
5869
private JavaFileScannerContext context;
5970

@@ -75,13 +86,86 @@ public void visitMethod(MethodTree methodTree) {
7586
return;
7687
}
7788

78-
if (isUnitTest(methodTree) && !isSpringBootSanityTest(methodTree) && !expectAssertion(methodTree) && !isLocalMethodWithAssertion(methodTree.symbol())) {
79-
context.reportIssue(this, methodTree.simpleName(), "Add at least one assertion to this test case.");
89+
if (isUnitTest(methodTree)) {
90+
if (isSpringBootAssertableContext(methodTree)) {
91+
return;
92+
}
93+
if (!isSpringBootSanityTest(methodTree) && !expectAssertion(methodTree) && !isLocalMethodWithAssertion(methodTree.symbol())) {
94+
context.reportIssue(this, methodTree.simpleName(), "Add at least one assertion to this test case.");
95+
}
96+
}
97+
}
98+
99+
private boolean isSpringBootAssertableContext(MethodTree methodTree) {
100+
var runMethodInvocation = SPRING_BOOT_APP_CTX_RUNNER_VISITOR.findMethodInvocation(methodTree);
101+
if (runMethodInvocation != null) {
102+
var contextConsumerImplSymbol = runMethodInvocation.arguments().get(0).symbolType().symbol();
103+
if (contextConsumerImplSymbol.isUnknown()) {
104+
// In this case we cannot know if the provided ContextConsumer has the type param <AssertableApplicationContext>, but we want to avoid FPs
105+
return true;
106+
}
107+
Type contextConsumerType;
108+
if (contextConsumerImplSymbol.isInterface()) {
109+
contextConsumerType = contextConsumerImplSymbol.type();
110+
} else {
111+
contextConsumerType = contextConsumerImplSymbol.interfaces().get(0);
112+
}
113+
return isAssertableApplicationContext(contextConsumerType) && hasDeclaredAssertions(contextConsumerImplSymbol);
114+
}
115+
return false;
116+
}
117+
118+
private static boolean isAssertableApplicationContext(Type contextConsumerType) {
119+
return contextConsumerType.typeArguments().get(0).is("org.springframework.boot.test.context.assertj.AssertableApplicationContext");
120+
}
121+
122+
/**
123+
* Takes a Symbol as input and checks if it has a declaring class available. If so, it will also check that the class has at least
124+
* one method with an assertion.
125+
* Used by {@link #isSpringBootAssertableContext(MethodTree)} to check if a ContextConsumer of AssertableApplicationContext
126+
* has at least an assertion in its methods.
127+
* @param contextConsumerImplSymbol The symbol for which we want to check the declaration of
128+
* @return true if the symbol has no declaration (to avoid FPs), or if its declaration is a class with at least one method with assertions
129+
*/
130+
private boolean hasDeclaredAssertions(Symbol contextConsumerImplSymbol) {
131+
Tree declaration = contextConsumerImplSymbol.declaration();
132+
if (declaration instanceof ClassTreeImpl contextConsumerImpl) {
133+
return contextConsumerImpl.members().stream()
134+
.anyMatch(m -> m instanceof MethodTree method && isLocalMethodWithAssertion(method.symbol()));
135+
}
136+
return true;
137+
}
138+
139+
/**
140+
* Finds the first nested method invocation in a tree that matches the MethodMatchers provided in the constructor
141+
*/
142+
private static class MethodInvocationMatcherVisitor extends BaseTreeVisitor {
143+
144+
private MethodInvocationTreeImpl methodInvocationTree;
145+
private final MethodMatchers matcher;
146+
147+
private MethodInvocationMatcherVisitor(MethodMatchers matcher) {
148+
this.matcher = matcher;
149+
}
150+
151+
@Override
152+
public void visitMethodInvocation(MethodInvocationTree tree) {
153+
if (matcher.matches(tree)) {
154+
methodInvocationTree = (MethodInvocationTreeImpl) tree;
155+
return;
156+
}
157+
super.visitMethodInvocation(tree);
158+
}
159+
160+
public MethodInvocationTreeImpl findMethodInvocation(Tree tree) {
161+
methodInvocationTree = null;
162+
tree.accept(this);
163+
return methodInvocationTree;
80164
}
81165
}
82166

83-
private static boolean isSpringBootSanityTest(MethodTree methodTree){
84-
if("contextLoads".equals(methodTree.simpleName().name())){
167+
private static boolean isSpringBootSanityTest(MethodTree methodTree) {
168+
if ("contextLoads".equals(methodTree.simpleName().name())) {
85169
ClassTree classTree = (ClassTree) methodTree.parent();
86170
return classTree.symbol().metadata().isAnnotatedWith("org.springframework.boot.test.context.SpringBootTest");
87171
}

java-checks/src/test/java/org/sonar/java/checks/tests/AssertionsInTestsCheckTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,13 @@ void testSpringBootSanity(){
117117
.withCheck(check)
118118
.verifyIssues();
119119
}
120+
121+
@Test
122+
void testSpringBootAssertableApplicationContext(){
123+
CheckVerifier.newVerifier()
124+
.onFile(testCodeSourcesPath("checks/tests/AssertionsInTestsCheck/SpringBootAppContextRunnerSample.java"))
125+
.withCheck(check)
126+
.verifyIssues();
127+
}
128+
120129
}

0 commit comments

Comments
 (0)