Skip to content

Commit

Permalink
Introduce Before/AfterArgumentSet lifecycle methods (#4366)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
marcphilipp authored Mar 5, 2025
1 parent 2e27ea7 commit 1e1f8d5
Show file tree
Hide file tree
Showing 21 changed files with 2,154 additions and 421 deletions.
2 changes: 2 additions & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,15 @@ endif::[]
:TempDir: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDir.html[@TempDir]
// Jupiter Params
:params-provider-package: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/package-summary.html[org.junit.jupiter.params.provider]
:AfterArgumentSet: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/AfterArgumentSet.html[@AfterArgumentSet]
:AnnotationBasedArgumentConverter: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.html[AnnotationBasedArgumentConverter]
:AnnotationBasedArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.html[AnnotationBasedArgumentsProvider]
:AggregateWith: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/AggregateWith.html[@AggregateWith]
:Arguments: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/Arguments.html[Arguments]
:ArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/ArgumentsProvider.html[ArgumentsProvider]
:ArgumentsAccessor: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAccessor.html[ArgumentsAccessor]
:ArgumentsAggregator: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAggregator.html[ArgumentsAggregator]
:BeforeArgumentSet: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/BeforeArgumentSet.html[@BeforeArgumentSet]
:CsvArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java[CsvArgumentsProvider]
:EmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/EmptySource.html[@EmptySource]
:FieldSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/FieldSource.html[@FieldSource]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ repository on GitHub.
supported with `@ParameterizedTest` may be used to provide arguments via constructor or
field injection. Please refer to the
<<../user-guide/index.adoc#writing-tests-parameterized-tests, User Guide>> for details.
* Introduce additional `@ParameterizedClass`-specific
`@BeforeArgumentSet`/`@AfterArgumentSet` lifecycle methods that are invoked once
before/after each set of arguments the class is invoked with.
* New `@SentenceFragment` annotation which allows one to supply custom text for individual
sentence fragments when using the `IndicativeSentences` `DisplayNameGenerator`. See the
updated documentation in the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
:testDir: ../../../../src/test/java

[[migrating-from-junit4]]
== Migrating from JUnit 4

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

[[migrating-from-junit4-tips-parameterized]]
==== Parameterized test classes

Unless `@UseParametersRunnerFactory` is used, a JUnit 4 parameterized test class can be
converted into a JUnit Jupiter
<<writing-tests-parameterized-tests, `@ParameterizedClass`>> by following these steps:

. Replace `@RunWith(Parameterized.class)` with `@ParameterizedClass`.
. Add a class-level `@MethodSource("methodName")` annotation where `methodName` is the
name of the method annotated with `@Parameters` and remove the `@Parameters` annotation
from the method.
. Replace `@BeforeParam`/`@AfterParam` with `@BeforeArgumentSet`/`@AfterArgumentSet`, if
there are any methods with such annotation. Moreover, if they declare parameters, set
the `injectArguments` annotation attribute to `true`.
. Change the imports of the `@Test` and `@Parameter` annotations to use the
`org.junit.jupiter.params` package.
. Change assertions etc. to use the `org.junit.jupiter.api` package as usual.
. Optionally, remove all `public` modifiers from the class and its methods and fields.

====
[source,java,indent=0]
.Before
----
include::{testDir}/example/ParameterizedMigrationDemo.java[tags=before]
----
[source,java,indent=0]
.After
----
include::{testDir}/example/ParameterizedMigrationDemo.java[tags=after]
----
====

[[migrating-from-junit4-rule-support]]
=== Limited JUnit 4 Rule Support
Expand Down
41 changes: 40 additions & 1 deletion documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ in the `junit-jupiter-api` module.
| `@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.
| `@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.
| `@ParameterizedClass` | Denotes that the annotated class is a <<writing-tests-parameterized-tests, parameterized class>>.
| `@BeforeArgumentSet` | Denotes that the annotated method should be executed once _before_ each set of arguments a `@ParameterizedClass` is invoked with.
| `@AfterArgumentSet` | Denotes that the annotated method should be executed once _after_ each set of arguments a `@ParameterizedClass` is invoked with.
| `@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>>.
| `@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.
| `@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.
Expand Down Expand Up @@ -1651,6 +1653,19 @@ If field injection is used, no constructor parameters will be resolved with argu
the source. Other <<writing-tests-dependency-injection, `ParameterResolver` extensions>>
may resolve constructor parameters as usual, though.

[[writing-tests-parameterized-tests-consuming-arguments-lifecycle-method]]
====== Lifecycle Methods

`{BeforeArgumentSet}` and `{AfterArgumentSet}` can also be used to consume arguments if
their `injectArguments` attribute is set to `true`. If so, their method signatures must
follow the same rules apply as defined for
<<writing-tests-parameterized-tests-consuming-arguments-methods, parameterized tests>> and
additionally use the same parameter types as the _indexed parameters_ of the parameterized
test class. Please refer to the Javadoc of `{BeforeArgumentSet}` and `{AfterArgumentSet}`
for details and to the
<<writing-tests-parameterized-tests-lifecycle-interop-classes, Lifecycle>> section for an
example.

[NOTE]
.AutoCloseable arguments
====
Expand Down Expand Up @@ -2671,7 +2686,31 @@ IDE.
You may use `ParameterResolver` extensions with `@ParameterizedClass` constructors.
However, if constructor injection is used, constructor parameters that are resolved by
argument sources need to come first in the parameter list. Values from argument sources
are not resolved for lifecycle methods (e.g. `@BeforeEach`).
are not resolved for regular lifecycle methods (e.g. `@BeforeEach`).

In addition to regular lifecycle methods, parameterized classes may declare
`{BeforeArgumentSet}` and `{AfterArgumentSet}` lifecycle methods that are called once
before/after each invocation of the parameterized class. These methods must be `static`
unless the parameterized class is configured to use `@TestInstance(Lifecycle.PER_CLASS)`
(see <<writing-tests-test-instance-lifecycle>>).

These lifecycle methods may optionally declare parameters that are resolved depending on
the setting of the `injectArguments` annotation attribute. If it is set to `false` (the
default), the parameters must be resolved by other registered {ParameterResolver}
extensions. If the attribute is set to `true`, the method may declare parameters that
match the arguments of the parameterized class (see the Javadoc of `{BeforeArgumentSet}`
and `{AfterArgumentSet}` for details). This may, for example, be used to initialize the
used arguments as demonstrated by the following example.

[source,java,indent=0]
.Using parameterized class lifecycle methods
----
include::{testRelease21Dir}/example/ParameterizedLifecycleDemo.java[tags=example]
----
<1> Initialization of the argument _before_ each invocation of the parameterized class
<2> Usage of the previously initialized argument in a test method
<3> Validation and cleanup of the argument _after_ each invocation of the parameterized
class

[[writing-tests-container-templates]]
=== Container Templates
Expand Down
101 changes: 101 additions & 0 deletions documentation/src/test/java/example/ParameterizedMigrationDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

import java.util.Arrays;

import org.junit.jupiter.params.AfterArgumentSet;
import org.junit.jupiter.params.BeforeArgumentSet;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

public class ParameterizedMigrationDemo {

@SuppressWarnings("JUnitMalformedDeclaration")
// tag::before[]
@RunWith(Parameterized.class)
// end::before[]
static
// tag::before[]
public class JUnit4ParameterizedClassTests {

@Parameterized.Parameters
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] { { 1, "foo" }, { 2, "bar" } });
}

// end::before[]
@SuppressWarnings("DefaultAnnotationParam")
// tag::before[]
@Parameterized.Parameter(0)
public int number;

@Parameterized.Parameter(1)
public String text;

@Parameterized.BeforeParam
public static void before(int number, String text) {
}

@Parameterized.AfterParam
public static void after() {
}

@org.junit.Test
public void someTest() {
}

@org.junit.Test
public void anotherTest() {
}
}
// end::before[]

@SuppressWarnings("JUnitMalformedDeclaration")
// tag::after[]
@ParameterizedClass
@MethodSource("data")
// end::after[]
static
// tag::after[]
class JupiterParameterizedClassTests {

static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] { { 1, "foo" }, { 2, "bar" } });
}

@org.junit.jupiter.params.Parameter(0)
int number;

@org.junit.jupiter.params.Parameter(1)
String text;

@BeforeArgumentSet(injectArguments = true)
static void before(int number, String text) {
}

@AfterArgumentSet
static void after() {
}

@org.junit.jupiter.api.Test
void someTest() {
}

@org.junit.jupiter.api.Test
void anotherTest() {
}
}
// end::after[]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.AfterArgumentSet;
import org.junit.jupiter.params.BeforeArgumentSet;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;

public class ParameterizedLifecycleDemo {

@Nested
// tag::example[]
@ParameterizedClass
@MethodSource("textFiles")
class TextFileTests {

static List<TextFile> textFiles() {
return List.of(
// tag::custom_line_break[]
new TextFile("file1", "first content"),
// tag::custom_line_break[]
new TextFile("file2", "second content")
// tag::custom_line_break[]
);
}

@Parameter
TextFile textFile;

@BeforeArgumentSet(injectArguments = true)
static void beforeArgumentSet(TextFile textFile, @TempDir Path tempDir) throws Exception {
var filePath = tempDir.resolve(textFile.fileName); // <1>
textFile.path = Files.writeString(filePath, textFile.content);
}

@AfterArgumentSet(injectArguments = true)
static void afterArgumentSet(TextFile textFile) throws Exception {
var actualContent = Files.readString(textFile.path); // <3>
assertEquals(textFile.content, actualContent, "Content must not have changed");
// Custom cleanup logic, if necessary
// File will be deleted automatically by @TempDir support
}

@Test
void test() {
assertTrue(Files.exists(textFile.path)); // <2>
}

@Test
void anotherTest() {
// ...
}

static class TextFile {

final String fileName;
final String content;
Path path;

TextFile(String fileName, String content) {
this.fileName = fileName;
this.content = content;
}

@Override
public String toString() {
return fileName;
}
}
}
// end::example[]

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@

import java.util.Arrays;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.CsvSource;

public class ParameterizedRecordDemo {

@SuppressWarnings("JUnitMalformedDeclaration")
@Nested
// tag::example[]
@ParameterizedClass
@CsvSource({ "apple, 23", "banana, 42" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,22 @@ private LifecycleMethodUtils() {
}

static List<Method> findBeforeAllMethods(Class<?> testClass, boolean requireStatic) {
return findMethodsAndAssertStaticAndNonPrivate(testClass, requireStatic, BeforeAll.class,
HierarchyTraversalMode.TOP_DOWN);
return findMethodsAndAssertStatic(testClass, requireStatic, BeforeAll.class, HierarchyTraversalMode.TOP_DOWN);
}

static List<Method> findAfterAllMethods(Class<?> testClass, boolean requireStatic) {
return findMethodsAndAssertStaticAndNonPrivate(testClass, requireStatic, AfterAll.class,
HierarchyTraversalMode.BOTTOM_UP);
return findMethodsAndAssertStatic(testClass, requireStatic, AfterAll.class, HierarchyTraversalMode.BOTTOM_UP);
}

static List<Method> findBeforeEachMethods(Class<?> testClass) {
return findMethodsAndAssertNonStaticAndNonPrivate(testClass, BeforeEach.class, HierarchyTraversalMode.TOP_DOWN);
return findMethodsAndAssertNonStatic(testClass, BeforeEach.class, HierarchyTraversalMode.TOP_DOWN);
}

static List<Method> findAfterEachMethods(Class<?> testClass) {
return findMethodsAndAssertNonStaticAndNonPrivate(testClass, AfterEach.class, HierarchyTraversalMode.BOTTOM_UP);
return findMethodsAndAssertNonStatic(testClass, AfterEach.class, HierarchyTraversalMode.BOTTOM_UP);
}

private static List<Method> findMethodsAndAssertStaticAndNonPrivate(Class<?> testClass, boolean requireStatic,
private static List<Method> findMethodsAndAssertStatic(Class<?> testClass, boolean requireStatic,
Class<? extends Annotation> annotationType, HierarchyTraversalMode traversalMode) {

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

private static List<Method> findMethodsAndAssertNonStaticAndNonPrivate(Class<?> testClass,
private static List<Method> findMethodsAndAssertNonStatic(Class<?> testClass,
Class<? extends Annotation> annotationType, HierarchyTraversalMode traversalMode) {

List<Method> methods = findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode);
Expand Down
Loading

0 comments on commit 1e1f8d5

Please sign in to comment.