Skip to content

Commit 3e0a0ef

Browse files
committed
Report discovery issue for missing @Nested annotations
Resolves #1736.
1 parent ea4b064 commit 3e0a0ef

File tree

4 files changed

+49
-6
lines changed

4 files changed

+49
-6
lines changed

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java

+21-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.junit.platform.commons.support.ReflectionSupport.findMethods;
2020
import static org.junit.platform.commons.support.ReflectionSupport.streamNestedClasses;
2121
import static org.junit.platform.commons.util.FunctionUtils.where;
22+
import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass;
2223
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
2324
import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.unresolved;
2425

@@ -44,6 +45,9 @@
4445
import org.junit.jupiter.engine.descriptor.TestClassAware;
4546
import org.junit.jupiter.engine.discovery.predicates.TestClassPredicates;
4647
import org.junit.platform.commons.support.ReflectionSupport;
48+
import org.junit.platform.commons.util.ReflectionUtils;
49+
import org.junit.platform.engine.DiscoveryIssue;
50+
import org.junit.platform.engine.DiscoveryIssue.Severity;
4751
import org.junit.platform.engine.DiscoverySelector;
4852
import org.junit.platform.engine.TestDescriptor;
4953
import org.junit.platform.engine.UniqueId;
@@ -52,6 +56,7 @@
5256
import org.junit.platform.engine.discovery.IterationSelector;
5357
import org.junit.platform.engine.discovery.NestedClassSelector;
5458
import org.junit.platform.engine.discovery.UniqueIdSelector;
59+
import org.junit.platform.engine.support.descriptor.ClassSource;
5560
import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter;
5661
import org.junit.platform.engine.support.discovery.SelectorResolver;
5762

@@ -63,12 +68,14 @@ class ClassSelectorResolver implements SelectorResolver {
6368
private final Predicate<String> classNameFilter;
6469
private final JupiterConfiguration configuration;
6570
private final TestClassPredicates predicates;
71+
private final DiscoveryIssueReporter issueReporter;
6672

6773
ClassSelectorResolver(Predicate<String> classNameFilter, JupiterConfiguration configuration,
6874
DiscoveryIssueReporter issueReporter) {
6975
this.classNameFilter = classNameFilter;
7076
this.configuration = configuration;
7177
this.predicates = new TestClassPredicates(issueReporter);
78+
this.issueReporter = issueReporter;
7279
}
7380

7481
@Override
@@ -98,9 +105,19 @@ private boolean isAcceptedStandaloneTestClass(Class<?> testClass) {
98105

99106
@Override
100107
public Resolution resolve(NestedClassSelector selector, Context context) {
101-
if (this.predicates.isAnnotatedWithNestedAndValid.test(selector.getNestedClass())) {
102-
return toResolution(context.addToParent(() -> selectClass(selector.getEnclosingClasses()),
103-
parent -> Optional.of(newMemberClassTestDescriptor(parent, selector.getNestedClass()))));
108+
Class<?> nestedClass = selector.getNestedClass();
109+
if (this.predicates.isAnnotatedWithNested.test(nestedClass)) {
110+
if (this.predicates.isValidNestedTestClass(nestedClass)) {
111+
return toResolution(context.addToParent(() -> selectClass(selector.getEnclosingClasses()),
112+
parent -> Optional.of(newMemberClassTestDescriptor(parent, nestedClass))));
113+
}
114+
}
115+
else if (isInnerClass(nestedClass) && predicates.looksLikeIntendedTestClass(nestedClass)) {
116+
String message = String.format(
117+
"Inner class '%s' looks like it was intended to be a test class but will not be executed. It must be static or annotated with @Nested.",
118+
nestedClass.getName());
119+
issueReporter.reportIssue(DiscoveryIssue.builder(Severity.WARNING, message) //
120+
.source(ClassSource.from(nestedClass)));
104121
}
105122
return unresolved();
106123
}
@@ -278,7 +295,7 @@ private Supplier<Set<? extends DiscoverySelector>> expansionCallback(TestDescrip
278295
this.predicates.isTestOrTestFactoryOrTestTemplateMethod, TOP_DOWN).stream() //
279296
.map(method -> selectMethod(testClasses, method));
280297
Stream<NestedClassSelector> nestedClasses = streamNestedClasses(testClass,
281-
this.predicates.isAnnotatedWithNestedAndValid) //
298+
this.predicates.isAnnotatedWithNested.or(ReflectionUtils::isInnerClass)) //
282299
.map(nestedClass -> DiscoverySelectors.selectNestedClass(testClasses, nestedClass));
283300
return Stream.concat(methods, nestedClasses).collect(
284301
toCollection((Supplier<Set<DiscoverySelector>>) LinkedHashSet::new));

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicates.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,14 @@ private boolean hasTestOrTestFactoryOrTestTemplateMethods(Class<?> candidate) {
8585
}
8686

8787
private boolean hasNestedTests(Class<?> candidate) {
88-
return isNestedClassPresent(candidate, this.isAnnotatedWithNestedAndValid);
88+
return isNestedClassPresent( //
89+
candidate, //
90+
isNotSame(candidate).and(
91+
this.isAnnotatedWithNested.or(it -> isInnerClass(it) && looksLikeIntendedTestClass(it))));
92+
}
93+
94+
private static Predicate<Class<?>> isNotSame(Class<?> candidate) {
95+
return clazz -> candidate != clazz;
8996
}
9097

9198
private static Condition<Class<?>> isNotPrivateUnlessAbstract(String prefix, DiscoveryIssueReporter issueReporter) {

jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoveryTests.java

+17
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,23 @@ static List<Named<LauncherDiscoveryRequest>> requestsForTestClassWithInvalidNest
267267
);
268268
}
269269

270+
@Test
271+
void reportsWarningForTestClassWithPotentialNestedTestClasses() {
272+
273+
var results = discoverTestsForClass(InvalidTestCases.class);
274+
275+
var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList();
276+
assertThat(discoveryIssues).hasSize(2);
277+
assertThat(discoveryIssues.getFirst().message()) //
278+
.isEqualTo(
279+
"Inner class '%s' looks like it was intended to be a test class but will not be executed. It must be static or annotated with @Nested.",
280+
InvalidTestCases.InvalidTestClassSubclassTestCase.class.getName());
281+
assertThat(discoveryIssues.getLast().message()) //
282+
.isEqualTo(
283+
"Inner class '%s' looks like it was intended to be a test class but will not be executed. It must be static or annotated with @Nested.",
284+
InvalidTestCases.InvalidTestClassTestCase.class.getName());
285+
}
286+
270287
// -------------------------------------------------------------------
271288

272289
@SuppressWarnings("unused")

jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicatesTests.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static org.assertj.core.api.Assertions.assertThat;
1414
import static org.junit.jupiter.api.Assertions.assertFalse;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
1516
import static org.junit.jupiter.api.Assertions.assertTrue;
1617

1718
import java.util.ArrayList;
@@ -23,6 +24,7 @@
2324
import org.junit.jupiter.api.Test;
2425
import org.junit.jupiter.api.TestFactory;
2526
import org.junit.jupiter.api.TestTemplate;
27+
import org.junit.platform.commons.JUnitException;
2628
import org.junit.platform.engine.DiscoveryIssue;
2729
import org.junit.platform.engine.DiscoveryIssue.Severity;
2830
import org.junit.platform.engine.support.descriptor.ClassSource;
@@ -216,7 +218,7 @@ void privateStaticTestClassEvaluatesToFalse() {
216218
*/
217219
@Test
218220
void recursiveHierarchies() {
219-
assertTrue(predicates.looksLikeIntendedTestClass(TestCases.OuterClass.class));
221+
assertThrows(JUnitException.class, () -> predicates.looksLikeIntendedTestClass(TestCases.OuterClass.class));
220222
assertTrue(predicates.isValidStandaloneTestClass(TestCases.OuterClass.class));
221223
assertThat(discoveryIssues).isEmpty();
222224

0 commit comments

Comments
 (0)