Skip to content

Commit e7f02d1

Browse files
authored
fix junit platform reporting for nested tests (fixes #1052, fixes #1234, via #1316)
1 parent dd81ea3 commit e7f02d1

3 files changed

Lines changed: 239 additions & 8 deletions

File tree

allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
import static io.qameta.allure.util.ResultsUtils.createHostLabel;
7474
import static io.qameta.allure.util.ResultsUtils.createLanguageLabel;
7575
import static io.qameta.allure.util.ResultsUtils.createPackageLabel;
76+
import static io.qameta.allure.util.ResultsUtils.createParentSuiteLabel;
77+
import static io.qameta.allure.util.ResultsUtils.createSubSuiteLabel;
7678
import static io.qameta.allure.util.ResultsUtils.createSuiteLabel;
7779
import static io.qameta.allure.util.ResultsUtils.createTestClassLabel;
7880
import static io.qameta.allure.util.ResultsUtils.createTestMethodLabel;
@@ -597,10 +599,7 @@ private void startTest(final TestIdentifier testIdentifier,
597599

598600
testSource.flatMap(AllureJunitPlatformUtils::getFullName).ifPresent(result::setFullName);
599601
testSource.map(this::getSourceLabels).ifPresent(result.getLabels()::addAll);
600-
testClass.ifPresent(aClass -> {
601-
final String suiteName = getDisplayName(aClass).orElse(aClass.getCanonicalName());
602-
result.getLabels().add(createSuiteLabel(suiteName));
603-
});
602+
testClass.map(this::getSuiteLabels).ifPresent(result.getLabels()::addAll);
604603

605604
final Optional<String> classDescription = testClass.flatMap(this::getDescription);
606605
final Optional<String> methodDescription = testMethod.flatMap(this::getDescription);
@@ -712,14 +711,59 @@ private List<String> getClassTitlePath(final Class<?> testClass) {
712711
.map(Package::getName)
713712
.orElse("");
714713
final List<String> result = ResultsUtils.createTitlePathFromPackage(packageName);
715-
final List<String> classNames = new ArrayList<>();
714+
getClassChain(testClass).stream()
715+
.map(clazz -> getDisplayName(clazz).orElse(clazz.getSimpleName()))
716+
.forEach(result::add);
717+
return result;
718+
}
719+
720+
/**
721+
* Returns suite labels for the given test class, derived from the class nesting chain.
722+
* A top-level class maps to the suite label only. Nested classes shift the chain across
723+
* the xUnit labels: the top-level class becomes the parent suite, the first nested class
724+
* the suite, and any deeper classes the sub suite, so the class nesting stays visible
725+
* in label-based report trees.
726+
*
727+
* @param testClass the test class
728+
* @return the suite labels
729+
*/
730+
private List<Label> getSuiteLabels(final Class<?> testClass) {
731+
final List<Class<?>> classChain = getClassChain(testClass);
732+
final Class<?> topLevelClass = classChain.get(0);
733+
final String topLevelName = getDisplayName(topLevelClass).orElse(topLevelClass.getCanonicalName());
734+
if (classChain.size() == 1) {
735+
return Collections.singletonList(createSuiteLabel(topLevelName));
736+
}
737+
final List<Label> labels = new ArrayList<>();
738+
labels.add(createParentSuiteLabel(topLevelName));
739+
labels.add(createSuiteLabel(getNestedDisplayName(classChain.get(1))));
740+
if (classChain.size() > 2) {
741+
final String subSuiteName = classChain.subList(2, classChain.size()).stream()
742+
.map(this::getNestedDisplayName)
743+
.collect(Collectors.joining(" > "));
744+
labels.add(createSubSuiteLabel(subSuiteName));
745+
}
746+
return labels;
747+
}
748+
749+
private String getNestedDisplayName(final Class<?> nestedClass) {
750+
return getDisplayName(nestedClass).orElse(nestedClass.getSimpleName());
751+
}
752+
753+
/**
754+
* Returns the class with its declaring classes, outermost first.
755+
*
756+
* @param testClass the test class
757+
* @return the class chain
758+
*/
759+
private static List<Class<?>> getClassChain(final Class<?> testClass) {
760+
final List<Class<?>> result = new ArrayList<>();
716761
Class<?> current = testClass;
717762
while (Objects.nonNull(current)) {
718-
classNames.add(getDisplayName(current).orElse(current.getSimpleName()));
763+
result.add(current);
719764
current = current.getDeclaringClass();
720765
}
721-
Collections.reverse(classNames);
722-
result.addAll(classNames);
766+
Collections.reverse(result);
723767
return result;
724768
}
725769

allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.qameta.allure.junitplatform.features.FailedTests;
3030
import io.qameta.allure.junitplatform.features.JupiterUniqueIdTest;
3131
import io.qameta.allure.junitplatform.features.MarkerAnnotationSupport;
32+
import io.qameta.allure.junitplatform.features.NestedDisplayNameTests;
3233
import io.qameta.allure.junitplatform.features.NestedTests;
3334
import io.qameta.allure.junitplatform.features.OneTest;
3435
import io.qameta.allure.junitplatform.features.OwnerTest;
@@ -97,8 +98,10 @@
9798
import static io.qameta.allure.util.ResultsUtils.HOST_LABEL_NAME;
9899
import static io.qameta.allure.util.ResultsUtils.OWNER_LABEL_NAME;
99100
import static io.qameta.allure.util.ResultsUtils.PACKAGE_LABEL_NAME;
101+
import static io.qameta.allure.util.ResultsUtils.PARENT_SUITE_LABEL_NAME;
100102
import static io.qameta.allure.util.ResultsUtils.SEVERITY_LABEL_NAME;
101103
import static io.qameta.allure.util.ResultsUtils.STORY_LABEL_NAME;
104+
import static io.qameta.allure.util.ResultsUtils.SUB_SUITE_LABEL_NAME;
102105
import static io.qameta.allure.util.ResultsUtils.SUITE_LABEL_NAME;
103106
import static io.qameta.allure.util.ResultsUtils.TAG_LABEL_NAME;
104107
import static io.qameta.allure.util.ResultsUtils.TEST_CLASS_LABEL_NAME;
@@ -984,6 +987,126 @@ void shouldSupportNestedClasses() {
984987
);
985988
}
986989

990+
@AllureFeatures.Trees
991+
@Issue("1234")
992+
@Issue("1052")
993+
@Test
994+
void shouldSetSuiteLabelsForNestedClasses() {
995+
final AllureResults allureResults = runClasses(NestedTests.class);
996+
997+
assertThat(allureResults.getTestResults())
998+
.filteredOn("name", "story1Test()")
999+
.flatExtracting(TestResult::getLabels)
1000+
.extracting(Label::getName, Label::getValue)
1001+
.contains(
1002+
tuple(PARENT_SUITE_LABEL_NAME, "io.qameta.allure.junitplatform.features.NestedTests"),
1003+
tuple(SUITE_LABEL_NAME, "Feature2"),
1004+
tuple(SUB_SUITE_LABEL_NAME, "Story1")
1005+
);
1006+
1007+
assertThat(allureResults.getTestResults())
1008+
.filteredOn("name", "feature1Test()")
1009+
.flatExtracting(TestResult::getLabels)
1010+
.extracting(Label::getName, Label::getValue)
1011+
.contains(
1012+
tuple(PARENT_SUITE_LABEL_NAME, "io.qameta.allure.junitplatform.features.NestedTests"),
1013+
tuple(SUITE_LABEL_NAME, "Feature1")
1014+
);
1015+
1016+
assertThat(allureResults.getTestResults())
1017+
.filteredOn("name", "feature1Test()")
1018+
.flatExtracting(TestResult::getLabels)
1019+
.extracting(Label::getName)
1020+
.doesNotContain(SUB_SUITE_LABEL_NAME);
1021+
1022+
assertThat(allureResults.getTestResults())
1023+
.filteredOn("name", "parentTest()")
1024+
.flatExtracting(TestResult::getLabels)
1025+
.extracting(Label::getName, Label::getValue)
1026+
.contains(tuple(SUITE_LABEL_NAME, "io.qameta.allure.junitplatform.features.NestedTests"));
1027+
1028+
assertThat(allureResults.getTestResults())
1029+
.filteredOn("name", "parentTest()")
1030+
.flatExtracting(TestResult::getLabels)
1031+
.extracting(Label::getName)
1032+
.doesNotContain(PARENT_SUITE_LABEL_NAME, SUB_SUITE_LABEL_NAME);
1033+
}
1034+
1035+
@AllureFeatures.Trees
1036+
@Issue("1234")
1037+
@Issue("1052")
1038+
@Test
1039+
void shouldUseDisplayNamesForNestedClassesSuiteStructure() {
1040+
final AllureResults allureResults = runClasses(NestedDisplayNameTests.class);
1041+
1042+
assertThat(allureResults.getTestResults())
1043+
.extracting(TestResult::getName)
1044+
.containsExactlyInAnyOrder(
1045+
"can be created with the dao",
1046+
"it must be saved to the dao",
1047+
"it can be fetched from the dao",
1048+
"it cannot be deleted by wrong id",
1049+
"it is still present"
1050+
);
1051+
1052+
assertThat(allureResults.getTestResults())
1053+
.filteredOn("name", "can be created with the dao")
1054+
.flatExtracting(TestResult::getLabels)
1055+
.extracting(Label::getName, Label::getValue)
1056+
.contains(tuple(SUITE_LABEL_NAME, "A customer object"));
1057+
1058+
assertThat(allureResults.getTestResults())
1059+
.filteredOn("name", "can be created with the dao")
1060+
.flatExtracting(TestResult::getLabels)
1061+
.extracting(Label::getName)
1062+
.doesNotContain(PARENT_SUITE_LABEL_NAME, SUB_SUITE_LABEL_NAME);
1063+
1064+
assertThat(allureResults.getTestResults())
1065+
.filteredOn("name", "it must be saved to the dao")
1066+
.flatExtracting(TestResult::getLabels)
1067+
.extracting(Label::getName, Label::getValue)
1068+
.contains(
1069+
tuple(PARENT_SUITE_LABEL_NAME, "A customer object"),
1070+
tuple(SUITE_LABEL_NAME, "when created")
1071+
);
1072+
1073+
assertThat(allureResults.getTestResults())
1074+
.filteredOn("name", "it must be saved to the dao")
1075+
.flatExtracting(TestResult::getLabels)
1076+
.extracting(Label::getName)
1077+
.doesNotContain(SUB_SUITE_LABEL_NAME);
1078+
1079+
assertThat(allureResults.getTestResults())
1080+
.filteredOn("name", "it can be fetched from the dao")
1081+
.flatExtracting(TestResult::getLabels)
1082+
.extracting(Label::getName, Label::getValue)
1083+
.contains(
1084+
tuple(PARENT_SUITE_LABEL_NAME, "A customer object"),
1085+
tuple(SUITE_LABEL_NAME, "when created"),
1086+
tuple(SUB_SUITE_LABEL_NAME, "after saving a customer")
1087+
);
1088+
1089+
assertThat(allureResults.getTestResults())
1090+
.filteredOn("name", "it is still present")
1091+
.flatExtracting(TestResult::getLabels)
1092+
.extracting(Label::getName, Label::getValue)
1093+
.contains(
1094+
tuple(PARENT_SUITE_LABEL_NAME, "A customer object"),
1095+
tuple(SUITE_LABEL_NAME, "when created"),
1096+
tuple(SUB_SUITE_LABEL_NAME, "after saving a customer > and reloading the dao")
1097+
);
1098+
1099+
assertThat(allureResults.getTestResults())
1100+
.filteredOn("name", "it can be fetched from the dao")
1101+
.extracting(TestResult::getTitlePath)
1102+
.containsExactly(
1103+
Arrays.asList(
1104+
"io", "qameta", "allure", "junitplatform", "features",
1105+
"A customer object", "when created", "after saving a customer"
1106+
)
1107+
);
1108+
}
1109+
9871110
@AllureFeatures.Attachments
9881111
@SuppressWarnings("OptionalGetWithoutIsPresent")
9891112
@Test
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2016-2026 Qameta Software Inc
3+
*
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+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
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 io.qameta.allure.junitplatform.features;
17+
18+
import org.junit.jupiter.api.DisplayName;
19+
import org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
22+
@DisplayName("A customer object")
23+
public class NestedDisplayNameTests {
24+
25+
@Test
26+
@DisplayName("can be created with the dao")
27+
void canBeCreated() {
28+
}
29+
30+
@Nested
31+
@DisplayName("when created")
32+
class WhenCreated {
33+
34+
@Test
35+
@DisplayName("it must be saved to the dao")
36+
void mustBeSaved() {
37+
}
38+
39+
@Nested
40+
@DisplayName("after saving a customer")
41+
class AfterSaving {
42+
43+
@Test
44+
@DisplayName("it can be fetched from the dao")
45+
void canBeFetched() {
46+
}
47+
48+
@Test
49+
@DisplayName("it cannot be deleted by wrong id")
50+
void cannotBeDeletedByWrongId() {
51+
}
52+
53+
@Nested
54+
@DisplayName("and reloading the dao")
55+
class AfterReload {
56+
57+
@Test
58+
@DisplayName("it is still present")
59+
void isStillPresent() {
60+
}
61+
}
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)