-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Overview
In the test suite for the Spring Framework, we have a TestExecutionListenersNestedTests
test class that is only intended to be run as a top-level test class. Specifically, if you run a @Test
in a @Nested
test class directly, that test will fail, because the @Test
methods in the enclosing test classes have not been run. Although that is perhaps questionable behavior, it is actually by design: this particular test fixture tests the interactions between JUnit Jupiter and the Spring TestContext Framework.
We also have SpringJUnitJupiterTestSuite
, which selects TestExecutionListenersNestedTests
via @SelectPackages("org.springframework.test.context.junit.jupiter")
and @IncludeClassNamePatterns(".*Tests$")
.
Historically SpringJUnitJupiterTestSuite
succeeded (with JUnit Jupiter versions prior to 5.13), but it now fails.
Since that setup is a bit complex, I have reduced this scenario to just use the following JUnit Jupiter extensions.
abstract class BaseExtension implements BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) throws Exception {
ExecutionOrderForNestedTests.extensions.add(getClass().getSimpleName());
}
}
class Foo extends BaseExtension {
}
class Bar extends BaseExtension {
}
class Baz extends BaseExtension {
}
class Qux extends BaseExtension {
}
And the simplified test class looks like this.
package example;
// imports...
@ExtendWith(Foo.class)
class ExecutionOrderForNestedTests {
static final String FOO = "Foo";
static final String BAR = "Bar";
static final String BAZ = "Baz";
static final String QUX = "Qux";
static final List<String> extensions = new ArrayList<>();
@AfterEach
void resetExtensions() {
extensions.clear();
}
@Test
void test() {
assertThat(extensions).containsExactly(FOO);
}
@Nested
class InheritedExtensionsTests {
@Test
void test() {
assertThat(extensions).containsExactly(FOO);
}
}
@Nested
@ExtendWith(Bar.class)
class InheritedAndLocalExtensionsTests {
@Test
void test() {
assertThat(extensions).containsExactly(FOO, BAR);
}
@Nested
@ExtendWith(Baz.class)
class DoubleNestedWithInheritedAndLocalExtensionsTests {
@Test
void test() {
assertThat(extensions).containsExactly(FOO, BAR, BAZ);
}
@Nested
@ExtendWith(Qux.class)
class TripleNestedWithInheritedAndLocalExtensionsTests {
@Test
void test() {
assertThat(extensions).containsExactly(FOO, BAR, BAZ, QUX);
}
}
}
}
}
If you run ExecutionOrderForNestedTests
by itself, all of the tests (including the tests in the @Nested
test classes) pass.
As I mentioned above, if you run an individual @Test
in a @Nested
test class directly, that will fail, but this is by design. So, that is not a regression.
The Regression
The regression is apparent only when running ExecutionOrderForNestedTests
via a @Suite
.
✅ This suite passes:
@Suite
@SelectPackages("example")
@IncludeClassNamePatterns(".+ExecutionOrderForNestedTests$")
class TestSuite {
}
✅ This suite also passes:
@Suite
@SelectClasses(ExecutionOrderForNestedTests.class)
class TestSuite {
}
❌ This suite fails:
@Suite
@SelectPackages("example")
class TestSuite {
}
❌ This suite also fails:
@Suite
@SelectPackages("example")
@IncludeClassNamePatterns(".+Tests$")
class TestSuite {
}
For example, TripleNestedWithInheritedAndLocalExtensionsTests.test()
fails as follows when run in one of those failing @Suite
scenarios.
org.opentest4j.AssertionFailedError:
Expecting actual:
["Foo", "Bar", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Qux"]
to contain exactly (and in same order):
["Foo", "Bar", "Baz", "Qux"]
but some elements were not expected:
["Foo", "Bar", "Foo", "Bar", "Baz"]
Analysis
If ExecutionOrderForNestedTests
is selected explicitly in the IDE or in a @Suite
(without discovering the individual @Nested
test classes in ExecutionOrderForNestedTests
), all tests pass.
If the @Nested
test classes in ExecutionOrderForNestedTests
are discovered in a @Suite
using classpath scanning via @SelectPackages
, the @Nested
test methods get executed before (or "independently of") the test methods in enclosing classes, which was not previously the case.