Skip to content

Commit 1e1f8d5

Browse files
authored
Introduce Before/AfterArgumentSet lifecycle methods (#4366)
This commit introduces two new annotations for ``@ParameterizedClass`-specific lifecycle methods. Methods annotated with ``@BeforeArgumentSet` or ``@AfterArgumentSet` are called once before or after, respectively, each argument set the parameterized class is invoked with. Depending on their `injectArguments` annotation attribute, they may consume the invocation's arguments, for example, to initialize them. Resolves #4352.
1 parent 2e27ea7 commit 1e1f8d5

21 files changed

+2154
-421
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,15 @@ endif::[]
185185
:TempDir: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDir.html[@TempDir]
186186
// Jupiter Params
187187
:params-provider-package: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/package-summary.html[org.junit.jupiter.params.provider]
188+
:AfterArgumentSet: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/AfterArgumentSet.html[@AfterArgumentSet]
188189
:AnnotationBasedArgumentConverter: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.html[AnnotationBasedArgumentConverter]
189190
:AnnotationBasedArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.html[AnnotationBasedArgumentsProvider]
190191
:AggregateWith: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/AggregateWith.html[@AggregateWith]
191192
:Arguments: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/Arguments.html[Arguments]
192193
:ArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/ArgumentsProvider.html[ArgumentsProvider]
193194
:ArgumentsAccessor: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAccessor.html[ArgumentsAccessor]
194195
:ArgumentsAggregator: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAggregator.html[ArgumentsAggregator]
196+
:BeforeArgumentSet: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/BeforeArgumentSet.html[@BeforeArgumentSet]
195197
:CsvArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java[CsvArgumentsProvider]
196198
:EmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/EmptySource.html[@EmptySource]
197199
:FieldSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/FieldSource.html[@FieldSource]

documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ repository on GitHub.
6363
supported with `@ParameterizedTest` may be used to provide arguments via constructor or
6464
field injection. Please refer to the
6565
<<../user-guide/index.adoc#writing-tests-parameterized-tests, User Guide>> for details.
66+
* Introduce additional `@ParameterizedClass`-specific
67+
`@BeforeArgumentSet`/`@AfterArgumentSet` lifecycle methods that are invoked once
68+
before/after each set of arguments the class is invoked with.
6669
* New `@SentenceFragment` annotation which allows one to supply custom text for individual
6770
sentence fragments when using the `IndicativeSentences` `DisplayNameGenerator`. See the
6871
updated documentation in the

documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
:testDir: ../../../../src/test/java
2+
13
[[migrating-from-junit4]]
24
== Migrating from JUnit 4
35

@@ -94,6 +96,8 @@ tests to JUnit Jupiter.
9496
- See also <<migrating-from-junit4-ignore-annotation-support>>.
9597
* `@Category` no longer exists; use `@Tag` instead.
9698
* `@RunWith` no longer exists; superseded by `@ExtendWith`.
99+
- For `@RunWith(Enclosed.class)` use `@Nested`.
100+
- For `@RunWith(Parameterized.class)` see <<migrating-from-junit4-tips-parameterized>>.
97101
* `@Rule` and `@ClassRule` no longer exist; superseded by `@ExtendWith` and
98102
`@RegisterExtension`.
99103
- See also <<migrating-from-junit4-rule-support>>.
@@ -105,6 +109,38 @@ tests to JUnit Jupiter.
105109
argument instead of the first one.
106110
- See <<migrating-from-junit4-failure-message-arguments>> for details.
107111

112+
[[migrating-from-junit4-tips-parameterized]]
113+
==== Parameterized test classes
114+
115+
Unless `@UseParametersRunnerFactory` is used, a JUnit 4 parameterized test class can be
116+
converted into a JUnit Jupiter
117+
<<writing-tests-parameterized-tests, `@ParameterizedClass`>> by following these steps:
118+
119+
. Replace `@RunWith(Parameterized.class)` with `@ParameterizedClass`.
120+
. Add a class-level `@MethodSource("methodName")` annotation where `methodName` is the
121+
name of the method annotated with `@Parameters` and remove the `@Parameters` annotation
122+
from the method.
123+
. Replace `@BeforeParam`/`@AfterParam` with `@BeforeArgumentSet`/`@AfterArgumentSet`, if
124+
there are any methods with such annotation. Moreover, if they declare parameters, set
125+
the `injectArguments` annotation attribute to `true`.
126+
. Change the imports of the `@Test` and `@Parameter` annotations to use the
127+
`org.junit.jupiter.params` package.
128+
. Change assertions etc. to use the `org.junit.jupiter.api` package as usual.
129+
. Optionally, remove all `public` modifiers from the class and its methods and fields.
130+
131+
====
132+
[source,java,indent=0]
133+
.Before
134+
----
135+
include::{testDir}/example/ParameterizedMigrationDemo.java[tags=before]
136+
----
137+
138+
[source,java,indent=0]
139+
.After
140+
----
141+
include::{testDir}/example/ParameterizedMigrationDemo.java[tags=after]
142+
----
143+
====
108144

109145
[[migrating-from-junit4-rule-support]]
110146
=== Limited JUnit 4 Rule Support

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ in the `junit-jupiter-api` module.
4444
| `@BeforeAll` | Denotes that the annotated method should be executed _before_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@BeforeClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <<writing-tests-test-instance-lifecycle, test instance lifecycle>> is used.
4545
| `@AfterAll` | Denotes that the annotated method should be executed _after_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@AfterClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <<writing-tests-test-instance-lifecycle, test instance lifecycle>> is used.
4646
| `@ParameterizedClass` | Denotes that the annotated class is a <<writing-tests-parameterized-tests, parameterized class>>.
47+
| `@BeforeArgumentSet` | Denotes that the annotated method should be executed once _before_ each set of arguments a `@ParameterizedClass` is invoked with.
48+
| `@AfterArgumentSet` | Denotes that the annotated method should be executed once _after_ each set of arguments a `@ParameterizedClass` is invoked with.
4749
| `@ContainerTemplate` | Denotes that the annotated class is a <<writing-tests-container-templates, template for a set of test cases>> designed to be executed multiple times depending on the number of invocation contexts returned by the registered <<extensions-container-templates, providers>>.
4850
| `@Nested` | Denotes that the annotated class is a non-static <<writing-tests-nested,nested test class>>. On Java 8 through Java 15, `@BeforeAll` and `@AfterAll` methods cannot be used directly in a `@Nested` test class unless the "per-class" <<writing-tests-test-instance-lifecycle, test instance lifecycle>> is used. Beginning with Java 16, `@BeforeAll` and `@AfterAll` methods can be declared as `static` in a `@Nested` test class with either test instance lifecycle mode. Such annotations are not inherited.
4951
| `@Tag` | Used to declare <<writing-tests-tagging-and-filtering,tags for filtering tests>>, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level.
@@ -1651,6 +1653,19 @@ If field injection is used, no constructor parameters will be resolved with argu
16511653
the source. Other <<writing-tests-dependency-injection, `ParameterResolver` extensions>>
16521654
may resolve constructor parameters as usual, though.
16531655

1656+
[[writing-tests-parameterized-tests-consuming-arguments-lifecycle-method]]
1657+
====== Lifecycle Methods
1658+
1659+
`{BeforeArgumentSet}` and `{AfterArgumentSet}` can also be used to consume arguments if
1660+
their `injectArguments` attribute is set to `true`. If so, their method signatures must
1661+
follow the same rules apply as defined for
1662+
<<writing-tests-parameterized-tests-consuming-arguments-methods, parameterized tests>> and
1663+
additionally use the same parameter types as the _indexed parameters_ of the parameterized
1664+
test class. Please refer to the Javadoc of `{BeforeArgumentSet}` and `{AfterArgumentSet}`
1665+
for details and to the
1666+
<<writing-tests-parameterized-tests-lifecycle-interop-classes, Lifecycle>> section for an
1667+
example.
1668+
16541669
[NOTE]
16551670
.AutoCloseable arguments
16561671
====
@@ -2671,7 +2686,31 @@ IDE.
26712686
You may use `ParameterResolver` extensions with `@ParameterizedClass` constructors.
26722687
However, if constructor injection is used, constructor parameters that are resolved by
26732688
argument sources need to come first in the parameter list. Values from argument sources
2674-
are not resolved for lifecycle methods (e.g. `@BeforeEach`).
2689+
are not resolved for regular lifecycle methods (e.g. `@BeforeEach`).
2690+
2691+
In addition to regular lifecycle methods, parameterized classes may declare
2692+
`{BeforeArgumentSet}` and `{AfterArgumentSet}` lifecycle methods that are called once
2693+
before/after each invocation of the parameterized class. These methods must be `static`
2694+
unless the parameterized class is configured to use `@TestInstance(Lifecycle.PER_CLASS)`
2695+
(see <<writing-tests-test-instance-lifecycle>>).
2696+
2697+
These lifecycle methods may optionally declare parameters that are resolved depending on
2698+
the setting of the `injectArguments` annotation attribute. If it is set to `false` (the
2699+
default), the parameters must be resolved by other registered {ParameterResolver}
2700+
extensions. If the attribute is set to `true`, the method may declare parameters that
2701+
match the arguments of the parameterized class (see the Javadoc of `{BeforeArgumentSet}`
2702+
and `{AfterArgumentSet}` for details). This may, for example, be used to initialize the
2703+
used arguments as demonstrated by the following example.
2704+
2705+
[source,java,indent=0]
2706+
.Using parameterized class lifecycle methods
2707+
----
2708+
include::{testRelease21Dir}/example/ParameterizedLifecycleDemo.java[tags=example]
2709+
----
2710+
<1> Initialization of the argument _before_ each invocation of the parameterized class
2711+
<2> Usage of the previously initialized argument in a test method
2712+
<3> Validation and cleanup of the argument _after_ each invocation of the parameterized
2713+
class
26752714

26762715
[[writing-tests-container-templates]]
26772716
=== Container Templates
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
import java.util.Arrays;
14+
15+
import org.junit.jupiter.params.AfterArgumentSet;
16+
import org.junit.jupiter.params.BeforeArgumentSet;
17+
import org.junit.jupiter.params.ParameterizedClass;
18+
import org.junit.jupiter.params.provider.MethodSource;
19+
import org.junit.runner.RunWith;
20+
import org.junit.runners.Parameterized;
21+
22+
public class ParameterizedMigrationDemo {
23+
24+
@SuppressWarnings("JUnitMalformedDeclaration")
25+
// tag::before[]
26+
@RunWith(Parameterized.class)
27+
// end::before[]
28+
static
29+
// tag::before[]
30+
public class JUnit4ParameterizedClassTests {
31+
32+
@Parameterized.Parameters
33+
public static Iterable<Object[]> data() {
34+
return Arrays.asList(new Object[][] { { 1, "foo" }, { 2, "bar" } });
35+
}
36+
37+
// end::before[]
38+
@SuppressWarnings("DefaultAnnotationParam")
39+
// tag::before[]
40+
@Parameterized.Parameter(0)
41+
public int number;
42+
43+
@Parameterized.Parameter(1)
44+
public String text;
45+
46+
@Parameterized.BeforeParam
47+
public static void before(int number, String text) {
48+
}
49+
50+
@Parameterized.AfterParam
51+
public static void after() {
52+
}
53+
54+
@org.junit.Test
55+
public void someTest() {
56+
}
57+
58+
@org.junit.Test
59+
public void anotherTest() {
60+
}
61+
}
62+
// end::before[]
63+
64+
@SuppressWarnings("JUnitMalformedDeclaration")
65+
// tag::after[]
66+
@ParameterizedClass
67+
@MethodSource("data")
68+
// end::after[]
69+
static
70+
// tag::after[]
71+
class JupiterParameterizedClassTests {
72+
73+
static Iterable<Object[]> data() {
74+
return Arrays.asList(new Object[][] { { 1, "foo" }, { 2, "bar" } });
75+
}
76+
77+
@org.junit.jupiter.params.Parameter(0)
78+
int number;
79+
80+
@org.junit.jupiter.params.Parameter(1)
81+
String text;
82+
83+
@BeforeArgumentSet(injectArguments = true)
84+
static void before(int number, String text) {
85+
}
86+
87+
@AfterArgumentSet
88+
static void after() {
89+
}
90+
91+
@org.junit.jupiter.api.Test
92+
void someTest() {
93+
}
94+
95+
@org.junit.jupiter.api.Test
96+
void anotherTest() {
97+
}
98+
}
99+
// end::after[]
100+
101+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.util.List;
19+
20+
import org.junit.jupiter.api.Nested;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.io.TempDir;
23+
import org.junit.jupiter.params.AfterArgumentSet;
24+
import org.junit.jupiter.params.BeforeArgumentSet;
25+
import org.junit.jupiter.params.Parameter;
26+
import org.junit.jupiter.params.ParameterizedClass;
27+
import org.junit.jupiter.params.provider.MethodSource;
28+
29+
public class ParameterizedLifecycleDemo {
30+
31+
@Nested
32+
// tag::example[]
33+
@ParameterizedClass
34+
@MethodSource("textFiles")
35+
class TextFileTests {
36+
37+
static List<TextFile> textFiles() {
38+
return List.of(
39+
// tag::custom_line_break[]
40+
new TextFile("file1", "first content"),
41+
// tag::custom_line_break[]
42+
new TextFile("file2", "second content")
43+
// tag::custom_line_break[]
44+
);
45+
}
46+
47+
@Parameter
48+
TextFile textFile;
49+
50+
@BeforeArgumentSet(injectArguments = true)
51+
static void beforeArgumentSet(TextFile textFile, @TempDir Path tempDir) throws Exception {
52+
var filePath = tempDir.resolve(textFile.fileName); // <1>
53+
textFile.path = Files.writeString(filePath, textFile.content);
54+
}
55+
56+
@AfterArgumentSet(injectArguments = true)
57+
static void afterArgumentSet(TextFile textFile) throws Exception {
58+
var actualContent = Files.readString(textFile.path); // <3>
59+
assertEquals(textFile.content, actualContent, "Content must not have changed");
60+
// Custom cleanup logic, if necessary
61+
// File will be deleted automatically by @TempDir support
62+
}
63+
64+
@Test
65+
void test() {
66+
assertTrue(Files.exists(textFile.path)); // <2>
67+
}
68+
69+
@Test
70+
void anotherTest() {
71+
// ...
72+
}
73+
74+
static class TextFile {
75+
76+
final String fileName;
77+
final String content;
78+
Path path;
79+
80+
TextFile(String fileName, String content) {
81+
this.fileName = fileName;
82+
this.content = content;
83+
}
84+
85+
@Override
86+
public String toString() {
87+
return fileName;
88+
}
89+
}
90+
}
91+
// end::example[]
92+
93+
}

documentation/src/test/java21/example/ParameterizedRecordDemo.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414

1515
import java.util.Arrays;
1616

17-
import org.junit.jupiter.api.Nested;
1817
import org.junit.jupiter.api.Test;
1918
import org.junit.jupiter.params.ParameterizedClass;
2019
import org.junit.jupiter.params.provider.CsvSource;
2120

2221
public class ParameterizedRecordDemo {
2322

2423
@SuppressWarnings("JUnitMalformedDeclaration")
25-
@Nested
2624
// tag::example[]
2725
@ParameterizedClass
2826
@CsvSource({ "apple, 23", "banana, 42" })

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,22 @@ private LifecycleMethodUtils() {
3737
}
3838

3939
static List<Method> findBeforeAllMethods(Class<?> testClass, boolean requireStatic) {
40-
return findMethodsAndAssertStaticAndNonPrivate(testClass, requireStatic, BeforeAll.class,
41-
HierarchyTraversalMode.TOP_DOWN);
40+
return findMethodsAndAssertStatic(testClass, requireStatic, BeforeAll.class, HierarchyTraversalMode.TOP_DOWN);
4241
}
4342

4443
static List<Method> findAfterAllMethods(Class<?> testClass, boolean requireStatic) {
45-
return findMethodsAndAssertStaticAndNonPrivate(testClass, requireStatic, AfterAll.class,
46-
HierarchyTraversalMode.BOTTOM_UP);
44+
return findMethodsAndAssertStatic(testClass, requireStatic, AfterAll.class, HierarchyTraversalMode.BOTTOM_UP);
4745
}
4846

4947
static List<Method> findBeforeEachMethods(Class<?> testClass) {
50-
return findMethodsAndAssertNonStaticAndNonPrivate(testClass, BeforeEach.class, HierarchyTraversalMode.TOP_DOWN);
48+
return findMethodsAndAssertNonStatic(testClass, BeforeEach.class, HierarchyTraversalMode.TOP_DOWN);
5149
}
5250

5351
static List<Method> findAfterEachMethods(Class<?> testClass) {
54-
return findMethodsAndAssertNonStaticAndNonPrivate(testClass, AfterEach.class, HierarchyTraversalMode.BOTTOM_UP);
52+
return findMethodsAndAssertNonStatic(testClass, AfterEach.class, HierarchyTraversalMode.BOTTOM_UP);
5553
}
5654

57-
private static List<Method> findMethodsAndAssertStaticAndNonPrivate(Class<?> testClass, boolean requireStatic,
55+
private static List<Method> findMethodsAndAssertStatic(Class<?> testClass, boolean requireStatic,
5856
Class<? extends Annotation> annotationType, HierarchyTraversalMode traversalMode) {
5957

6058
List<Method> methods = findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode);
@@ -64,7 +62,7 @@ private static List<Method> findMethodsAndAssertStaticAndNonPrivate(Class<?> tes
6462
return methods;
6563
}
6664

67-
private static List<Method> findMethodsAndAssertNonStaticAndNonPrivate(Class<?> testClass,
65+
private static List<Method> findMethodsAndAssertNonStatic(Class<?> testClass,
6866
Class<? extends Annotation> annotationType, HierarchyTraversalMode traversalMode) {
6967

7068
List<Method> methods = findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode);

0 commit comments

Comments
 (0)