Skip to content

Commit ed99088

Browse files
authored
Merge pull request #19094 from jcogs33/jcogs33/java/junit5-missing-nested-annotation
Java: Add new quality query to detect missing `@Nested` annotation in JUnit5 tests
2 parents 7ee862a + 07a694e commit ed99088

File tree

15 files changed

+371
-1
lines changed

15 files changed

+371
-1
lines changed

java/ql/integration-tests/java/query-suite/java-code-quality.qls.expected

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ql/java/ql/src/Likely Bugs/Comparison/IncomparableEquals.ql
55
ql/java/ql/src/Likely Bugs/Comparison/InconsistentEqualsHashCode.ql
66
ql/java/ql/src/Likely Bugs/Comparison/MissingInstanceofInEquals.ql
77
ql/java/ql/src/Likely Bugs/Comparison/RefEqBoxed.ql
8+
ql/java/ql/src/Likely Bugs/Frameworks/JUnit/JUnit5MissingNestedAnnotation.ql
89
ql/java/ql/src/Likely Bugs/Likely Typos/ContradictoryTypeChecks.ql
910
ql/java/ql/src/Likely Bugs/Likely Typos/SuspiciousDateFormat.ql
1011
ql/java/ql/src/Likely Bugs/Resource Leaks/CloseReader.ql

java/ql/lib/semmle/code/java/UnitTests.qll

+51
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,57 @@ class JUnitJupiterTestMethod extends Method {
125125
}
126126
}
127127

128+
/**
129+
* A JUnit 5 test method.
130+
*
131+
* A test method is defined by JUnit as "any instance method
132+
* that is directly annotated or meta-annotated with `@Test`,
133+
* `@RepeatedTest`, `@ParameterizedTest`, `@TestFactory`, or
134+
* `@TestTemplate`."
135+
*
136+
* See https://junit.org/junit5/docs/current/user-guide/#writing-tests-definitions
137+
*/
138+
class JUnit5TestMethod extends Method {
139+
JUnit5TestMethod() {
140+
this instanceof JUnitJupiterTestMethod or
141+
this.getAnAnnotation()
142+
.getType()
143+
.hasQualifiedName("org.junit.jupiter.api", ["RepeatedTest", "TestFactory", "TestTemplate"]) or
144+
this.getAnAnnotation()
145+
.getType()
146+
.hasQualifiedName("org.junit.jupiter.params", "ParameterizedTest")
147+
}
148+
}
149+
150+
/**
151+
* A JUnit 5 test class.
152+
*
153+
* A test class must contain at least one test method, and
154+
* cannot be abstract.
155+
*
156+
* See https://junit.org/junit5/docs/current/user-guide/#writing-tests-definitions
157+
*/
158+
class JUnit5TestClass extends Class {
159+
JUnit5TestClass() {
160+
this.getAMethod() instanceof JUnit5TestMethod and
161+
not this.isAbstract()
162+
}
163+
}
164+
165+
/**
166+
* A JUnit inner test class that is non-anonymous, non-local,
167+
* and non-private.
168+
*/
169+
class JUnit5InnerTestClass extends JUnit5TestClass {
170+
JUnit5InnerTestClass() {
171+
// `InnerClass` is a non-static nested class.
172+
this instanceof InnerClass and
173+
not this.isAnonymous() and
174+
not this.isLocal() and
175+
not this.isPrivate()
176+
}
177+
}
178+
128179
/**
129180
* A JUnit `@Ignore` annotation.
130181
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
## Overview
2+
3+
JUnit tests are grouped in a class, and starting from JUnit 5, users can group the test classes in a larger class so they can share the local environment of the enclosing class. While this helps organize the unit tests and foster code reuse, if an inner test class is not annotated with `@Nested`, the unit tests in it will fail to execute during builds.
4+
5+
## Recommendation
6+
7+
If you want the tests defined in an inner class to be recognized by the build plugin and be executed, annotate the class with `@Nested`, imported from `org.junit.jupiter.api`.
8+
9+
## Example
10+
11+
```java
12+
import org.junit.jupiter.api.Nested;
13+
import static org.junit.Assert.assertEquals;
14+
15+
public class IntegerOperationTest {
16+
private int i; // Shared variable among the inner classes.
17+
18+
@BeforeEach
19+
public void initTest() { i = 0; }
20+
21+
@Nested
22+
public class AdditionTest { // COMPLIANT: Inner test class annotated with `@Nested`.
23+
@Test
24+
public void addTest1() {
25+
assertEquals(1, i + 1);
26+
}
27+
}
28+
29+
public class SubtractionTest { // NON_COMPLIANT: Inner test class missing `@Nested`.
30+
@Test
31+
public void addTest1() {
32+
assertEquals(-1, i - 1);
33+
}
34+
}
35+
}
36+
```
37+
38+
## Implementation Notes
39+
40+
This rule is focused on missing `@Nested` annotations on non-static nested (inner) test classes. Static nested test classes should not be annotated with `@Nested`. As a result, the absence of a `@Nested` annotation on such classes is compliant. Identifying incorrect application of a `@Nested` annotation to static nested classes is out of scope for this rule.
41+
42+
## References
43+
44+
- JUnit 5 API Documentation: [Annotation Interface Nested](https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Nested.html).
45+
- JUnit 5 User Guide: [Nested Tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @id java/junit5-missing-nested-annotation
3+
* @previous-id java/junit5-non-static-inner-class-missing-nested-annotation
4+
* @name Missing `@Nested` annotation on JUnit 5 inner test class
5+
* @description A JUnit 5 inner test class that is missing a `@Nested` annotation will be
6+
* excluded from execution and may indicate a mistake from the
7+
* programmer.
8+
* @kind problem
9+
* @precision very-high
10+
* @problem.severity warning
11+
* @tags quality
12+
* reliability
13+
* correctness
14+
* testability
15+
* frameworks/junit
16+
*/
17+
18+
import java
19+
20+
from JUnit5InnerTestClass testClass
21+
where not testClass.hasAnnotation("org.junit.jupiter.api", "Nested")
22+
select testClass, "This JUnit 5 inner test class lacks a '@Nested' annotation."

java/ql/src/codeql-suites/java-code-quality.qls

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
- java/inconsistent-equals-and-hashcode
77
- java/input-resource-leak
88
- java/integer-multiplication-cast-to-long
9+
- java/junit5-missing-nested-annotation
910
- java/output-resource-leak
1011
- java/reference-equality-of-boxed-types
1112
- java/string-replace-all-with-non-regex
1213
- java/suspicious-date-format
1314
- java/type-variable-hides-type
1415
- java/unchecked-cast-in-equals
15-
- java/unused-container
16+
- java/unused-container
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import java.util.Collection;
2+
import org.junit.jupiter.api.Test;
3+
import org.junit.jupiter.api.RepeatedTest;
4+
import org.junit.jupiter.api.TestFactory;
5+
import org.junit.jupiter.api.TestTemplate;
6+
import org.junit.jupiter.api.Nested;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.ValueSource;
9+
10+
public class AnnotationTest {
11+
@Nested
12+
public class Test1 { // COMPLIANT: Inner test class has `@Nested`
13+
@Test
14+
public void test() {
15+
}
16+
}
17+
18+
// NON_COMPLIANT: Inner test class is missing `@Nested`
19+
public class Test2_Test { // $ Alert
20+
@Test
21+
public void test() {
22+
}
23+
}
24+
25+
// NON_COMPLIANT: Inner test class is missing `@Nested`
26+
public class Test2_RepeatedTest { // $ Alert
27+
@RepeatedTest(2)
28+
public void test() {
29+
}
30+
}
31+
32+
// NON_COMPLIANT: Inner test class is missing `@Nested`
33+
public class Test2_ParameterizedTest { // $ Alert
34+
@ParameterizedTest
35+
@ValueSource(strings = { "" })
36+
public void test(String s) {
37+
}
38+
}
39+
40+
// NON_COMPLIANT: Inner test class is missing `@Nested`
41+
public class Test2_TestFactory { // $ Alert
42+
@TestFactory
43+
Collection<Object> test() {
44+
return null;
45+
}
46+
}
47+
48+
// NON_COMPLIANT: Inner test class is missing `@Nested`
49+
public class Test2_TestTemplate { // $ Alert
50+
@TestTemplate
51+
public void test() {
52+
}
53+
}
54+
55+
public class Test3 { // COMPLIANT: Since it is empty, it is not a test class
56+
}
57+
58+
public class Test4 { // COMPLIANT: Since no methods have `@Test`, it is not a test class
59+
public void f() {
60+
}
61+
62+
public void g() {
63+
}
64+
65+
public void h() {
66+
}
67+
}
68+
69+
public static class Test5 { // COMPLIANT: Static nested test classes don't need `@Nested`
70+
@Test
71+
public void test() {
72+
}
73+
}
74+
75+
// COMPLIANT: Invalid to use `@Nested` on a static class, but
76+
// this matter is out of scope (see QHelp Implementation Notes)
77+
@Nested
78+
public static class Test6 {
79+
@Test
80+
public void test() {
81+
}
82+
}
83+
84+
public abstract class Test7 { // COMPLIANT: Abstract nested test classes don't need `@Nested`
85+
@Test
86+
public void test() {
87+
}
88+
}
89+
90+
interface Test8 {
91+
}
92+
93+
public void f() {
94+
// COMPLIANT: anonymous classes are not considered as inner test
95+
// classes by JUnit and therefore don't need `@Nested`
96+
new Test8() {
97+
@Test
98+
public void test() {
99+
}
100+
};
101+
// COMPLIANT: local classes are not considered as inner test
102+
// classes by JUnit and therefore don't need `@Nested`
103+
class Test9 {
104+
@Test
105+
void test() {
106+
}
107+
}
108+
}
109+
110+
// COMPLIANT: private classes are not considered as inner test
111+
// classes by JUnit and therefore don't need `@Nested`
112+
private class Test10 {
113+
@Test
114+
public void test() {
115+
}
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| AnnotationTest.java:19:16:19:25 | Test2_Test | This JUnit 5 inner test class lacks a '@Nested' annotation. |
2+
| AnnotationTest.java:26:16:26:33 | Test2_RepeatedTest | This JUnit 5 inner test class lacks a '@Nested' annotation. |
3+
| AnnotationTest.java:33:16:33:38 | Test2_ParameterizedTest | This JUnit 5 inner test class lacks a '@Nested' annotation. |
4+
| AnnotationTest.java:41:16:41:32 | Test2_TestFactory | This JUnit 5 inner test class lacks a '@Nested' annotation. |
5+
| AnnotationTest.java:49:16:49:33 | Test2_TestTemplate | This JUnit 5 inner test class lacks a '@Nested' annotation. |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: Likely Bugs/Frameworks/JUnit/JUnit5MissingNestedAnnotation.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/junit-jupiter-api-5.2.0

java/ql/test/stubs/junit-jupiter-api-5.2.0/org/junit/jupiter/api/Nested.java

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

java/ql/test/stubs/junit-jupiter-api-5.2.0/org/junit/jupiter/api/RepeatedTest.java

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

java/ql/test/stubs/junit-jupiter-api-5.2.0/org/junit/jupiter/api/TestFactory.java

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

java/ql/test/stubs/junit-jupiter-api-5.2.0/org/junit/jupiter/api/TestTemplate.java

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

java/ql/test/stubs/junit-jupiter-api-5.2.0/org/junit/jupiter/params/ParameterizedTest.java

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)