Repository: junit-team/junit5 (language: java)

Issue:
Add lifecycle methods for parameterized containers
Follow-up on #4320.

In addition to the existing `@BeforeAll`, `@BeforeEach`, `@AfterEach`, and `@AfterAll` lifecycle methods, we should add introduce `@BeforeArgumentSet`/`@AfterArgumentSet` annotations that can be declared on `static` (unless `TestInstance.Lifecycle.PER_CLASS` is used) methods of the test class that will be executed once _per argument set_ before/after all test methods and nested classes in a `@ParameterizedContainer` class.

Since those methods are usually `static`, they won't have access to the resolved parameters. Therefore, and for compatibility with JUnit 4's `@BeforeParams`/`@AfterParams` methods, we should support injecting them as method parameters of those lifecycle methods.

## Deliverables

- [x] Introduce `@BeforeArgumentSet`/`@AfterArgumentSet` lifecycle methods for `@ParameterizedContainer` classes
- [x] Add support for passing the resolved arguments of the current invocation to those lifecycle methods


Discussion / hints:
Depends on #4320 

Fix patch:
diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc
index 58c016917350..007485f8db1e 100644
--- a/documentation/src/docs/asciidoc/link-attributes.adoc
+++ b/documentation/src/docs/asciidoc/link-attributes.adoc
@@ -185,6 +185,7 @@ 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]
@@ -192,6 +193,7 @@ endif::[]
 :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]
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc
index 7c400cb60757..3c513d5f7ab8 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc
@@ -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
diff --git a/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc b/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc
index 29290532756a..999d53503e75 100644
--- a/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc
+++ b/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc
@@ -1,3 +1,5 @@
+:testDir: ../../../../src/test/java
+
 [[migrating-from-junit4]]
 == Migrating from JUnit 4
 
@@ -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>>.
@@ -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
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java
index 7d76ba1a5a16..83f60a06ef9a 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java
@@ -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);
@@ -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);
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AbstractArgumentSetLifecycleMethodInvoker.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AbstractArgumentSetLifecycleMethodInvoker.java
new file mode 100644
index 000000000000..d159a9ef4bb2
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AbstractArgumentSetLifecycleMethodInvoker.java
@@ -0,0 +1,72 @@
+/*
+ * 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 org.junit.jupiter.params;
+
+import org.junit.jupiter.api.extension.ExecutableInvoker;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+
+/**
+ * @since 5.13
+ */
+abstract class AbstractArgumentSetLifecycleMethodInvoker implements ParameterResolver {
+
+	private final ParameterizedClassContext declarationContext;
+	private final EvaluatedArgumentSet arguments;
+	private final int invocationIndex;
+	private final ResolutionCache resolutionCache;
+	private final ArgumentSetLifecycleMethod lifecycleMethod;
+
+	AbstractArgumentSetLifecycleMethodInvoker(ParameterizedClassContext declarationContext,
+			EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache,
+			ArgumentSetLifecycleMethod lifecycleMethod) {
+		this.declarationContext = declarationContext;
+		this.arguments = arguments;
+		this.invocationIndex = invocationIndex;
+		this.resolutionCache = resolutionCache;
+		this.lifecycleMethod = lifecycleMethod;
+	}
+
+	@Override
+	public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) {
+		return ExtensionContextScope.TEST_METHOD;
+	}
+
+	@Override
+	public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
+			throws ParameterResolutionException {
+		return parameterContext.getDeclaringExecutable().equals(this.lifecycleMethod.method) //
+				&& this.lifecycleMethod.parameterResolver.supports(parameterContext);
+	}
+
+	@Override
+	public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
+			throws ParameterResolutionException {
+		return this.lifecycleMethod.parameterResolver //
+				.resolve(parameterContext, extensionContext, this.arguments, this.invocationIndex,
+					this.resolutionCache);
+	}
+
+	protected void invoke(ExtensionContext context) {
+		if (isCorrectTestClass(context)) {
+			ExecutableInvoker executableInvoker = context.getExecutableInvoker();
+			Object testInstance = context.getTestInstance().orElse(null);
+			executableInvoker.invoke(this.lifecycleMethod.method, testInstance);
+		}
+	}
+
+	private boolean isCorrectTestClass(ExtensionContext context) {
+		return this.declarationContext.getAnnotatedElement().equals(context.getTestClass().orElse(null));
+	}
+
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterArgumentSet.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterArgumentSet.java
new file mode 100644
index 000000000000..f133c43bff39
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterArgumentSet.java
@@ -0,0 +1,162 @@
+/*
+ * 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 org.junit.jupiter.params;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apiguardian.api.API;
+import org.junit.jupiter.params.aggregator.AggregateWith;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+
+/**
+ * {@code @AfterArgumentSet} is used to signal that the annotated method should
+ * be executed <em>before</em> <strong>each</strong> set of arguments of the
+ * current {@link ParameterizedClass @ParameterizedClass}.
+ *
+ * <p>Declaring {@code @AfterArgumentSet} methods in a regular,
+ * non-parameterized test class has no effect and will be ignored.
+ *
+ * <h2>Method Signatures</h2>
+ *
+ * <p>{@code @AfterArgumentSet} methods must have a {@code void} return type,
+ * must not be private, and must be {@code static} by default. Consequently,
+ * {@code @AfterArgumentSet} methods are not supported in
+ * {@link org.junit.jupiter.api.Nested @Nested} test classes or as <em>interface
+ * default methods</em> unless the test class is annotated with
+ * {@link org.junit.jupiter.api.TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
+ * However, beginning with Java 16 {@code @AfterArgumentSet} methods may be
+ * declared as {@code static} in {@link org.junit.jupiter.api.Nested @Nested}
+ * test classes, in which case the {@code Lifecycle.PER_CLASS} restriction no
+ * longer applies.
+ *
+ * <h2>Method Arguments</h2>
+ *
+ * <p>{@code @AfterArgumentSet} methods may optionally declare parameters that
+ * are resolved depending on the setting of the {@link #injectArguments()}
+ * attribute.
+ *
+ * <p>If {@link #injectArguments()} is set to {@code false} (the default), the
+ * parameters must be resolved by other registered
+ * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}.
+ *
+ * <p>If {@link #injectArguments()} is set to {@code true}, the method must
+ * declare the same parameters, in the same order, as the <em>indexed
+ * parameters</em> (see {@link ParameterizedClass @ParameterizedClass}) of the
+ * parameterized test class. It may declare a subset of the indexed parameters
+ * starting from the first argument. Additionally, the method may declare custom
+ * <em>aggregator parameters</em> (see
+ * {@link ParameterizedClass @ParameterizedClass}) at the end of its parameter
+ * list. If the method declares additional parameters after these aggregator
+ * parameters, or more parameters than the class has indexed
+ * parameters, they may be resolved by other
+ * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}.
+ *
+ * <p>For example, given a {@link ParameterizedClass @ParameterizedClass} with
+ * <em>indexed parameters</em> of type {@code int} and {@code String}, the
+ * following method signatures are valid:
+ *
+ * <pre>{@code
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet() { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(int number) { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(int number, String text) { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(int number, String text, TestInfo testInfo) { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(ArgumentsAccessor accessor) { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(int number, String text, ArgumentsAccessor accessor) { ... }
+ *
+ * @AfterArgumentSet(injectArguments = true)
+ * void afterArgumentSet(int number, String text, ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ * }</pre>
+ *
+ * <p>In the snippet above,{@link ArgumentsAccessor} is used as an example of an
+ * aggregator parameter but the same applies to any parameter annotated with
+ * {@link AggregateWith @AggregateWith}. The parameter of type
+ * {@link org.junit.jupiter.api.TestInfo TestInfo} is used as an example of a
+ * parameter that is resolved by another
+ * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver}.
+ *
+ * <h2>Inheritance and Execution Order</h2>
+ *
+ * <p>{@code @AfterArgumentSet} methods are inherited from superclasses as long
+ * as they are not <em>overridden</em> according to the visibility rules of the
+ * Java language. Furthermore, {@code @AfterArgumentSet} methods from
+ * superclasses will be executed before {@code @AfterArgumentSet} methods in
+ * subclasses.
+ *
+ * <p>Similarly, {@code @AfterArgumentSet} methods declared in an interface are
+ * inherited as long as they are not overridden, and {@code @AfterArgumentSet}
+ * methods from an interface will be executed before {@code @AfterArgumentSet}
+ * methods in the class that implements the interface.
+ *
+ * <p>JUnit Jupiter does not guarantee the execution order of multiple
+ * {@code @AfterArgumentSet} methods that are declared within a single
+ * parameterized test class or test interface. While it may at times appear that
+ * these methods are invoked in alphabetical order, they are in fact sorted
+ * using an algorithm that is deterministic but intentionally non-obvious.
+ *
+ * <p>In addition, {@code @AfterArgumentSet} methods are in no way linked to
+ * {@code @BeforeArgumentSet} methods. Consequently, there are no guarantees
+ * with regard to their <em>wrapping</em> behavior. For example, given two
+ * {@code @AfterArgumentSet} methods {@code createA()} and {@code createB()} as
+ * well as two {@code @BeforeArgumentSet} methods {@code destroyA()} and
+ * {@code destroyB()}, the order in which the {@code @AfterArgumentSet} methods
+ * are executed (e.g. {@code createA()} before {@code createB()}) does not imply
+ * any order for the seemingly corresponding {@code @BeforeArgumentSet} methods.
+ * In other words, {@code destroyA()} might be called before <em>or</em> after
+ * {@code destroyB()}. The JUnit Team therefore recommends that developers
+ * declare at most one {@code @AfterArgumentSet} method and at most one
+ * {@code @BeforeArgumentSet} method per test class or test interface unless
+ * there are no dependencies between the {@code @AfterArgumentSet} methods or
+ * between the {@code @BeforeArgumentSet} methods.
+ *
+ * <h2>Composition</h2>
+ *
+ * <p>{@code @AfterArgumentSet} may be used as a meta-annotation in order to
+ * create a custom <em>composed annotation</em> that inherits the semantics of
+ * {@code @AfterArgumentSet}.
+ *
+ * @since 5.13
+ * @see ParameterizedClass
+ * @see BeforeArgumentSet
+ * @see org.junit.jupiter.api.TestInstance
+ */
+@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@API(status = EXPERIMENTAL, since = "5.13")
+public @interface AfterArgumentSet {
+
+	/**
+	 * Whether the arguments of the parameterized test class should be injected
+	 * into the annotated method (defaults to {@code false}).
+	 */
+	boolean injectArguments() default false;
+
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterArgumentSetMethodInvoker.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterArgumentSetMethodInvoker.java
new file mode 100644
index 000000000000..4666371b9702
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterArgumentSetMethodInvoker.java
@@ -0,0 +1,32 @@
+/*
+ * 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 org.junit.jupiter.params;
+
+import org.junit.jupiter.api.extension.AfterContainerTemplateInvocationCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * @since 5.13
+ */
+class AfterArgumentSetMethodInvoker extends AbstractArgumentSetLifecycleMethodInvoker
+		implements AfterContainerTemplateInvocationCallback {
+
+	AfterArgumentSetMethodInvoker(ParameterizedClassContext declarationContext, EvaluatedArgumentSet arguments,
+			int invocationIndex, ResolutionCache resolutionCache, ArgumentSetLifecycleMethod lifecycleMethod) {
+		super(declarationContext, arguments, invocationIndex, resolutionCache, lifecycleMethod);
+	}
+
+	@Override
+	public void afterContainerTemplateInvocation(ExtensionContext context) {
+		invoke(context);
+	}
+
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentSetLifecycleMethod.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentSetLifecycleMethod.java
new file mode 100644
index 000000000000..13ed374ebf14
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentSetLifecycleMethod.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.junit.jupiter.params;
+
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.util.Preconditions;
+
+/**
+ * @since 5.13
+ */
+class ArgumentSetLifecycleMethod {
+
+	final Method method;
+	final ParameterResolver parameterResolver;
+
+	ArgumentSetLifecycleMethod(Method method) {
+		this(method, ParameterResolver.DISABLED);
+	}
+
+	ArgumentSetLifecycleMethod(Method method, ParameterResolver parameterResolver) {
+		this.method = Preconditions.notNull(method, "method must not be null");
+		this.parameterResolver = Preconditions.notNull(parameterResolver, "parameterResolver must not be null");
+	}
+
+	interface ParameterResolver {
+
+		ParameterResolver DISABLED = new ParameterResolver() {
+			@Override
+			public boolean supports(ParameterContext parameterContext) {
+				return false;
+			}
+
+			@Override
+			public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext,
+					EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) {
+				throw new JUnitException("Parameter resolution is disabled");
+			}
+		};
+
+		boolean supports(ParameterContext parameterContext);
+
+		Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache);
+
+	}
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeArgumentSet.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeArgumentSet.java
new file mode 100644
index 000000000000..69f81fcf838c
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeArgumentSet.java
@@ -0,0 +1,162 @@
+/*
+ * 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 org.junit.jupiter.params;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apiguardian.api.API;
+import org.junit.jupiter.params.aggregator.AggregateWith;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+
+/**
+ * {@code @BeforeArgumentSet} is used to signal that the annotated method should
+ * be executed <em>before</em> <strong>each</strong> set of arguments of the
+ * current {@link ParameterizedClass @ParameterizedClass}.
+ *
+ * <p>Declaring {@code @BeforeArgumentSet} methods in a regular,
+ * non-parameterized test class has no effect and will be ignored.
+ *
+ * <h2>Method Signatures</h2>
+ *
+ * <p>{@code @BeforeArgumentSet} methods must have a {@code void} return type,
+ * must not be private, and must be {@code static} by default. Consequently,
+ * {@code @BeforeArgumentSet} methods are not supported in
+ * {@link org.junit.jupiter.api.Nested @Nested} test classes or as <em>interface
+ * default methods</em> unless the test class is annotated with
+ * {@link org.junit.jupiter.api.TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
+ * However, beginning with Java 16 {@code @BeforeArgumentSet} methods may be
+ * declared as {@code static} in {@link org.junit.jupiter.api.Nested @Nested}
+ * test classes, in which case the {@code Lifecycle.PER_CLASS} restriction no
+ * longer applies.
+ *
+ * <h2>Method Arguments</h2>
+ *
+ * <p>{@code @BeforeArgumentSet} methods may optionally declare parameters that
+ * are resolved depending on the setting of the {@link #injectArguments()}
+ * attribute.
+ *
+ * <p>If {@link #injectArguments()} is set to {@code false} (the default), the
+ * parameters must be resolved by other registered
+ * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}.
+ *
+ * <p>If {@link #injectArguments()} is set to {@code true}, the method must
+ * declare the same parameters, in the same order, as the <em>indexed
+ * parameters</em> (see {@link ParameterizedClass @ParameterizedClass}) of the
+ * parameterized test class. It may declare a subset of the indexed parameters
+ * starting from the first argument. Additionally, the method may declare custom
+ * <em>aggregator parameters</em> (see
+ * {@link ParameterizedClass @ParameterizedClass}) at the end of its parameter
+ * list. If the method declares additional parameters after these aggregator
+ * parameters, or more parameters than the class has indexed
+ * parameters, they may be resolved by other
+ * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}.
+ *
+ * <p>For example, given a {@link ParameterizedClass @ParameterizedClass} with
+ * <em>indexed parameters</em> of type {@code int} and {@code String}, the
+ * following method signatures are valid:
+ *
+ * <pre>{@code
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet() { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(int number) { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(int number, String text) { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(int number, String text, TestInfo testInfo) { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(ArgumentsAccessor accessor) { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(int number, String text, ArgumentsAccessor accessor) { ... }
+ *
+ * @BeforeArgumentSet(injectArguments = true)
+ * void beforeArgumentSet(int number, String text, ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ * }</pre>
+ *
+ * <p>In the snippet above,{@link ArgumentsAccessor} is used as an example of an
+ * aggregator parameter but the same applies to any parameter annotated with
+ * {@link AggregateWith @AggregateWith}. The parameter of type
+ * {@link org.junit.jupiter.api.TestInfo TestInfo} is used as an example of a
+ * parameter that is resolved by another
+ * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver}.
+ *
+ * <h2>Inheritance and Execution Order</h2>
+ *
+ * <p>{@code @BeforeArgumentSet} methods are inherited from superclasses as long
+ * as they are not <em>overridden</em> according to the visibility rules of the
+ * Java language. Furthermore, {@code @BeforeArgumentSet} methods from
+ * superclasses will be executed before {@code @BeforeArgumentSet} methods in
+ * subclasses.
+ *
+ * <p>Similarly, {@code @BeforeArgumentSet} methods declared in an interface are
+ * inherited as long as they are not overridden, and {@code @BeforeArgumentSet}
+ * methods from an interface will be executed before {@code @BeforeArgumentSet}
+ * methods in the class that implements the interface.
+ *
+ * <p>JUnit Jupiter does not guarantee the execution order of multiple
+ * {@code @BeforeArgumentSet} methods that are declared within a single
+ * parameterized test class or test interface. While it may at times appear that
+ * these methods are invoked in alphabetical order, they are in fact sorted
+ * using an algorithm that is deterministic but intentionally non-obvious.
+ *
+ * <p>In addition, {@code @BeforeArgumentSet} methods are in no way linked to
+ * {@code @AfterArgumentSet} methods. Consequently, there are no guarantees with
+ * regard to their <em>wrapping</em> behavior. For example, given two
+ * {@code @BeforeArgumentSet} methods {@code createA()} and {@code createB()} as
+ * well as two {@code @AfterArgumentSet} methods {@code destroyA()} and
+ * {@code destroyB()}, the order in which the {@code @BeforeArgumentSet} methods
+ * are executed (e.g. {@code createA()} before {@code createB()}) does not imply
+ * any order for the seemingly corresponding {@code @AfterArgumentSet} methods.
+ * In other words, {@code destroyA()} might be called before <em>or</em> after
+ * {@code destroyB()}. The JUnit Team therefore recommends that developers
+ * declare at most one {@code @BeforeArgumentSet} method and at most one
+ * {@code @AfterArgumentSet} method per test class or test interface unless
+ * there are no dependencies between the {@code @BeforeArgumentSet} methods or
+ * between the {@code @AfterArgumentSet} methods.
+ *
+ * <h2>Composition</h2>
+ *
+ * <p>{@code @BeforeArgumentSet} may be used as a meta-annotation in order to
+ * create a custom <em>composed annotation</em> that inherits the semantics of
+ * {@code @BeforeArgumentSet}.
+ *
+ * @since 5.13
+ * @see ParameterizedClass
+ * @see AfterArgumentSet
+ * @see org.junit.jupiter.api.TestInstance
+ */
+@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@API(status = EXPERIMENTAL, since = "5.13")
+public @interface BeforeArgumentSet {
+
+	/**
+	 * Whether the arguments of the parameterized test class should be injected
+	 * into the annotated method (defaults to {@code false}).
+	 */
+	boolean injectArguments() default false;
+
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeArgumentSetMethodInvoker.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeArgumentSetMethodInvoker.java
new file mode 100644
index 000000000000..7e336c72f0a8
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeArgumentSetMethodInvoker.java
@@ -0,0 +1,32 @@
+/*
+ * 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 org.junit.jupiter.params;
+
+import org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * @since 5.13
+ */
+class BeforeArgumentSetMethodInvoker extends AbstractArgumentSetLifecycleMethodInvoker
+		implements BeforeContainerTemplateInvocationCallback {
+
+	BeforeArgumentSetMethodInvoker(ParameterizedClassContext declarationContext, EvaluatedArgumentSet arguments,
+			int invocationIndex, ResolutionCache resolutionCache, ArgumentSetLifecycleMethod lifecycleMethod) {
+		super(declarationContext, arguments, invocationIndex, resolutionCache, lifecycleMethod);
+	}
+
+	@Override
+	public void beforeContainerTemplateInvocation(ExtensionContext context) {
+		invoke(context);
+	}
+
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java
deleted file mode 100644
index c9a175653bd3..000000000000
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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 org.junit.jupiter.params;
-
-import java.util.Optional;
-
-import org.junit.jupiter.params.support.ParameterDeclaration;
-
-/**
- * @since 5.13
- */
-class ExecutableParameterDeclaration implements ParameterDeclaration {
-
-	private final java.lang.reflect.Parameter parameter;
-	private final int index;
-
-	ExecutableParameterDeclaration(java.lang.reflect.Parameter parameter, int index) {
-		this.parameter = parameter;
-		this.index = index;
-	}
-
-	@Override
-	public java.lang.reflect.Parameter getAnnotatedElement() {
-		return this.parameter;
-	}
-
-	@Override
-	public Class<?> getParameterType() {
-		return this.parameter.getType();
-	}
-
-	@Override
-	public int getParameterIndex() {
-		return this.index;
-	}
-
-	@Override
-	public Optional<String> getParameterName() {
-		return this.parameter.isNamePresent() ? Optional.of(this.parameter.getName()) : Optional.empty();
-	}
-}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java
deleted file mode 100644
index f493a0996e4c..000000000000
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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 org.junit.jupiter.params;
-
-import java.lang.reflect.Field;
-import java.util.Optional;
-
-import org.junit.jupiter.params.support.FieldContext;
-import org.junit.jupiter.params.support.ParameterDeclaration;
-
-/**
- * @since 5.13
- */
-class FieldParameterDeclaration implements ParameterDeclaration, FieldContext {
-
-	private final Field field;
-	private final int index;
-
-	FieldParameterDeclaration(Field field, int index) {
-		this.field = field;
-		this.index = index;
-	}
-
-	@Override
-	public Field getField() {
-		return this.field;
-	}
-
-	@Override
-	public Field getAnnotatedElement() {
-		return this.field;
-	}
-
-	@Override
-	public Class<?> getParameterType() {
-		return this.field.getType();
-	}
-
-	@Override
-	public int getParameterIndex() {
-		return index;
-	}
-
-	@Override
-	public Optional<String> getParameterName() {
-		return Optional.of(this.field.getName());
-	}
-
-}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java
index 57ca9bc84045..5614424ae264 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java
@@ -106,6 +106,14 @@
  * conversion to the target type automatically (see the User Guide for further
  * details).
  *
+ * <h2>Lifecycle Methods</h2>
+ *
+ * <p>If you wish to execute custom code before or after each set of arguments
+ * is used to invoke the parameterized class, you may declare methods annotated
+ * with {@link BeforeArgumentSet @BeforeArgumentSet} or
+ * {@link AfterArgumentSet @AfterArgumentSet}. This can, for example, be useful
+ * to initialize the arguments before they are used.
+ *
  * <h2>Composed Annotations</h2>
  *
  * <p>{@code @ParameterizedClass} may also be used as a meta-annotation in
@@ -122,6 +130,8 @@
  *
  * @since 5.13
  * @see Parameter
+ * @see BeforeArgumentSet
+ * @see AfterArgumentSet
  * @see ParameterizedTest
  * @see org.junit.jupiter.params.provider.Arguments
  * @see org.junit.jupiter.params.provider.ArgumentsProvider
@@ -149,7 +159,8 @@
 	 * The display name to be used for individual invocations of the
 	 * parameterized class; never blank or consisting solely of whitespace.
 	 *
-	 * <p>Defaults to <code>{@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}</code>.
+	 * <p>Defaults to
+	 * <code>{@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}</code>.
 	 *
 	 * <p>If the default display name flag
 	 * (<code>{@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}</code>)
@@ -161,7 +172,9 @@
 	 * Gradle and Maven), a JVM system property, or the JUnit Platform configuration
 	 * file (i.e., a file named {@code junit-platform.properties} in the root of
 	 * the class path). Consult the User Guide for further information.</li>
-	 * <li>Otherwise, <code>{@value ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME}</code> will be used.</li>
+	 * <li>Otherwise,
+	 * <code>{@value ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME}</code>
+	 * will be used.</li>
 	 * </ul>
 	 *
 	 * <h4>Supported placeholders</h4>
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java
index bbac39648d4b..a13c583126b5 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java
@@ -11,17 +11,31 @@
 package org.junit.jupiter.params;
 
 import static java.util.Collections.emptyList;
+import static java.util.Collections.reverse;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
 import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated;
 import static org.junit.platform.commons.support.HierarchyTraversalMode.BOTTOM_UP;
+import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN;
 import static org.junit.platform.commons.support.ReflectionSupport.findFields;
+import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
 import static org.junit.platform.commons.util.ReflectionUtils.isRecordClass;
+import static org.junit.platform.commons.util.ReflectionUtils.returnsPrimitiveVoid;
 
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Predicate;
 
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+import org.junit.platform.commons.support.ModifierSupport;
 import org.junit.platform.commons.util.ReflectionUtils;
 
 class ParameterizedClassContext implements ParameterizedDeclarationContext<ContainerTemplateInvocationContext> {
@@ -31,6 +45,8 @@ class ParameterizedClassContext implements ParameterizedDeclarationContext<Conta
 	private final TestInstance.Lifecycle testInstanceLifecycle;
 	private final ResolverFacade resolverFacade;
 	private final InjectionType injectionType;
+	private final List<ArgumentSetLifecycleMethod> beforeMethods;
+	private final List<ArgumentSetLifecycleMethod> afterMethods;
 
 	ParameterizedClassContext(Class<?> clazz, ParameterizedClass annotation,
 			TestInstance.Lifecycle testInstanceLifecycle) {
@@ -47,6 +63,20 @@ class ParameterizedClassContext implements ParameterizedDeclarationContext<Conta
 			this.resolverFacade = ResolverFacade.create(clazz, fields);
 			this.injectionType = InjectionType.FIELDS;
 		}
+
+		this.beforeMethods = findLifecycleMethodsAndAssertStaticAndNonPrivate(clazz, testInstanceLifecycle, TOP_DOWN,
+			BeforeArgumentSet.class, BeforeArgumentSet::injectArguments, this.resolverFacade);
+
+		// Make a local copy since findAnnotatedMethods() returns an immutable list.
+		this.afterMethods = new ArrayList<>(
+			findLifecycleMethodsAndAssertStaticAndNonPrivate(clazz, testInstanceLifecycle, BOTTOM_UP,
+				AfterArgumentSet.class, AfterArgumentSet::injectArguments, this.resolverFacade));
+
+		// Since the bottom-up ordering of afterMethods will later be reversed when the
+		// AfterArgumentSetMethodInvoker extensions are executed within
+		// ContainerTemplateInvocationTestDescriptor, we have to reverse them to put them
+		// in top-down order before we register them as extensions.
+		reverse(afterMethods);
 	}
 
 	private static List<Field> findParameterAnnotatedFields(Class<?> clazz) {
@@ -105,6 +135,75 @@ InjectionType getInjectionType() {
 		return injectionType;
 	}
 
+	List<ArgumentSetLifecycleMethod> getBeforeMethods() {
+		return beforeMethods;
+	}
+
+	List<ArgumentSetLifecycleMethod> getAfterMethods() {
+		return afterMethods;
+	}
+
+	private static <A extends Annotation> List<ArgumentSetLifecycleMethod> findLifecycleMethodsAndAssertStaticAndNonPrivate(
+			Class<?> testClass, TestInstance.Lifecycle testInstanceLifecycle, HierarchyTraversalMode traversalMode,
+			Class<A> annotationType, Predicate<A> injectArgumentsPredicate, ResolverFacade resolverFacade) {
+
+		List<Method> methods = findMethodsAndCheckVoidReturnTypeAndNonPrivate(testClass, annotationType, traversalMode);
+
+		return methods.stream() //
+				.peek(method -> {
+					if (testInstanceLifecycle != PER_CLASS) {
+						assertStatic(annotationType, method);
+					}
+				}) //
+				.map(method -> {
+					A annotation = getAnnotation(method, annotationType);
+					if (injectArgumentsPredicate.test(annotation)) {
+						return new ArgumentSetLifecycleMethod(method,
+							resolverFacade.createLifecycleMethodParameterResolver(method, annotation));
+					}
+					return new ArgumentSetLifecycleMethod(method);
+				}) //
+				.collect(toUnmodifiableList());
+	}
+
+	private static <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) {
+		return AnnotationSupport.findAnnotation(method, annotationType) //
+				.orElseThrow(() -> new JUnitException("Method not annotated with @" + annotationType.getSimpleName()));
+	}
+
+	private static List<Method> findMethodsAndCheckVoidReturnTypeAndNonPrivate(Class<?> testClass,
+			Class<? extends Annotation> annotationType, HierarchyTraversalMode traversalMode) {
+
+		List<Method> methods = findAnnotatedMethods(testClass, annotationType, traversalMode);
+		methods.forEach(method -> {
+			assertVoid(annotationType, method);
+			assertNonPrivate(annotationType, method);
+		});
+		return methods;
+	}
+
+	private static void assertStatic(Class<? extends Annotation> annotationType, Method method) {
+		if (ModifierSupport.isNotStatic(method)) {
+			throw new JUnitException(String.format(
+				"@%s method '%s' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).",
+				annotationType.getSimpleName(), method.toGenericString()));
+		}
+	}
+
+	private static void assertVoid(Class<? extends Annotation> annotationType, Method method) {
+		if (!returnsPrimitiveVoid(method)) {
+			throw new JUnitException(String.format("@%s method '%s' must not return a value.",
+				annotationType.getSimpleName(), method.toGenericString()));
+		}
+	}
+
+	private static void assertNonPrivate(Class<? extends Annotation> annotationType, Method method) {
+		if (ModifierSupport.isPrivate(method)) {
+			throw new JUnitException(String.format("@%s method '%s' must not be private.",
+				annotationType.getSimpleName(), method.toGenericString()));
+		}
+	}
+
 	enum InjectionType {
 		CONSTRUCTOR, FIELDS
 	}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java
index 76df5c9c5ce5..6b1d643d028e 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java
@@ -10,10 +10,11 @@
 
 package org.junit.jupiter.params;
 
-import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
 import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD;
 
 import java.util.List;
+import java.util.stream.Stream;
 
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext;
@@ -41,12 +42,22 @@ public String getDisplayName(int invocationIndex) {
 
 	@Override
 	public List<Extension> getAdditionalExtensions() {
+		return Stream.concat(Stream.of(createParameterInjector()), createLifecycleMethodInvokers()) //
+				.collect(toList());
+	}
+
+	@Override
+	public void prepareInvocation(ExtensionContext context) {
+		super.prepareInvocation(context);
+	}
+
+	private Extension createParameterInjector() {
 		InjectionType injectionType = this.declarationContext.getInjectionType();
 		switch (injectionType) {
 			case CONSTRUCTOR:
-				return singletonList(createExtensionForConstructorInjection());
+				return createExtensionForConstructorInjection();
 			case FIELDS:
-				return singletonList(createExtensionForFieldInjection());
+				return createExtensionForFieldInjection();
 		}
 		throw new JUnitException("Unsupported injection type: " + injectionType);
 	}
@@ -72,9 +83,21 @@ private Extension createExtensionForFieldInjection() {
 		throw new JUnitException("Unsupported lifecycle: " + lifecycle);
 	}
 
-	@Override
-	public void prepareInvocation(ExtensionContext context) {
-		super.prepareInvocation(context);
+	private Stream<Extension> createLifecycleMethodInvokers() {
+		return Stream.concat( //
+			this.declarationContext.getBeforeMethods().stream().map(this::createBeforeArgumentSetMethodInvoker), //
+			this.declarationContext.getAfterMethods().stream().map(this::createAfterArgumentSetMethodInvoker) //
+		);
+	}
+
+	private BeforeArgumentSetMethodInvoker createBeforeArgumentSetMethodInvoker(ArgumentSetLifecycleMethod method) {
+		return new BeforeArgumentSetMethodInvoker(this.declarationContext, this.arguments, this.invocationIndex,
+			this.resolutionCache, method);
+	}
+
+	private AfterArgumentSetMethodInvoker createAfterArgumentSetMethodInvoker(ArgumentSetLifecycleMethod method) {
+		return new AfterArgumentSetMethodInvoker(this.declarationContext, this.arguments, this.invocationIndex,
+			this.resolutionCache, method);
 	}
 
 }
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java
index 55a775621b0b..7ea70fadf834 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java
@@ -26,6 +26,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -34,9 +35,11 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import org.junit.jupiter.api.extension.AnnotatedElementContext;
+import org.junit.jupiter.api.extension.ExtensionConfigurationException;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.jupiter.api.extension.ParameterResolutionException;
@@ -55,6 +58,7 @@
 import org.junit.jupiter.params.support.ParameterDeclarations;
 import org.junit.platform.commons.JUnitException;
 import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.commons.function.Try;
 import org.junit.platform.commons.support.ModifierSupport;
 import org.junit.platform.commons.util.Preconditions;
 import org.junit.platform.commons.util.StringUtils;
@@ -73,7 +77,7 @@ static ResolverFacade create(Class<?> clazz, List<Field> fields) {
 			int index = annotation.value();
 
 			FieldParameterDeclaration declaration = new FieldParameterDeclaration(field, annotation.value());
-			if (isAggregator(declaration)) {
+			if (declaration.isAggregator()) {
 				aggregatorParameters.add(declaration);
 			}
 			else {
@@ -102,7 +106,7 @@ static ResolverFacade create(Constructor<?> constructor, ParameterizedClass anno
 		return create(constructor, annotation, implicitParameters);
 	}
 
-	static ResolverFacade create(Method method, ParameterizedTest annotation) {
+	static ResolverFacade create(Method method, Annotation annotation) {
 		return create(method, annotation, 0);
 	}
 
@@ -120,13 +124,13 @@ static ResolverFacade create(Method method, ParameterizedTest annotation) {
 	 * </ol>
 	 */
 	private static ResolverFacade create(Executable executable, Annotation annotation, int indexOffset) {
-		NavigableMap<Integer, ParameterDeclaration> indexedParameters = new TreeMap<>();
-		NavigableMap<Integer, ParameterDeclaration> aggregatorParameters = new TreeMap<>();
+		NavigableMap<Integer, ExecutableParameterDeclaration> indexedParameters = new TreeMap<>();
+		NavigableMap<Integer, ExecutableParameterDeclaration> aggregatorParameters = new TreeMap<>();
 		java.lang.reflect.Parameter[] parameters = executable.getParameters();
 		for (int index = indexOffset; index < parameters.length; index++) {
-			ParameterDeclaration declaration = new ExecutableParameterDeclaration(parameters[index],
-				index - indexOffset);
-			if (isAggregator(declaration)) {
+			ExecutableParameterDeclaration declaration = new ExecutableParameterDeclaration(parameters[index], index,
+				indexOffset);
+			if (declaration.isAggregator()) {
 				Preconditions.condition(
 					aggregatorParameters.isEmpty()
 							|| aggregatorParameters.lastKey() == declaration.getParameterIndex() - 1,
@@ -149,11 +153,11 @@ else if (aggregatorParameters.isEmpty()) {
 	private final int parameterIndexOffset;
 	private final Map<ParameterDeclaration, Resolver> resolvers;
 	private final DefaultParameterDeclarations indexedParameterDeclarations;
-	private final Set<? extends ParameterDeclaration> aggregatorParameters;
+	private final Set<? extends ResolvableParameterDeclaration> aggregatorParameters;
 
 	private ResolverFacade(AnnotatedElement sourceElement,
-			NavigableMap<Integer, ? extends ParameterDeclaration> indexedParameters,
-			Set<? extends ParameterDeclaration> aggregatorParameters, int parameterIndexOffset) {
+			NavigableMap<Integer, ? extends ResolvableParameterDeclaration> indexedParameters,
+			Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset) {
 		this.aggregatorParameters = aggregatorParameters;
 		this.parameterIndexOffset = parameterIndexOffset;
 		this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size());
@@ -215,6 +219,25 @@ int determineConsumedArgumentCount(EvaluatedArgumentSet arguments) {
 		return arguments.getTotalLength();
 	}
 
+	ArgumentSetLifecycleMethod.ParameterResolver createLifecycleMethodParameterResolver(Method method,
+			Annotation annotation) {
+		ResolverFacade originalResolverFacade = this;
+		ResolverFacade lifecycleMethodResolverFacade = create(method, annotation);
+
+		Map<ParameterDeclaration, ResolvableParameterDeclaration> parameterDeclarationMapping = new HashMap<>();
+		List<String> errors = validateLifecycleMethodParameters(method, annotation, originalResolverFacade,
+			lifecycleMethodResolverFacade, parameterDeclarationMapping);
+
+		return Try //
+				.call(() -> configurationErrorOrSuccess(errors,
+					() -> new DefaultArgumentSetLifecycleMethodParameterResolver(originalResolverFacade,
+						lifecycleMethodResolverFacade, parameterDeclarationMapping))) //
+				.getOrThrow(cause -> new ExtensionConfigurationException(
+					String.format("Invalid @%s lifecycle method declaration: %s",
+						annotation.annotationType().getSimpleName(), method.toGenericString()),
+					cause));
+	}
+
 	/**
 	 * Resolve the parameter for the supplied context using the supplied
 	 * arguments.
@@ -223,14 +246,23 @@ Object resolve(ParameterContext parameterContext, ExtensionContext extensionCont
 			int invocationIndex, ResolutionCache resolutionCache) {
 
 		int parameterIndex = toLogicalIndex(parameterContext);
-		ParameterDeclaration declaration = this.indexedParameterDeclarations.get(parameterIndex) //
-				.orElseGet(() -> this.aggregatorParameters.stream().filter(
-					it -> it.getParameterIndex() == parameterIndex).findFirst() //
-						.orElseThrow(() -> new ParameterResolutionException(
-							"Parameter index out of bounds: " + parameterIndex)));
+		ResolvableParameterDeclaration declaration = findDeclaration(parameterIndex) //
+				.orElseThrow(
+					() -> new ParameterResolutionException("Parameter index out of bounds: " + parameterIndex));
+
 		return resolutionCache.resolve(declaration,
-			() -> getResolver(extensionContext, declaration, parameterContext.getParameter()) //
-					.resolve(parameterContext, parameterIndex, arguments, invocationIndex));
+			() -> resolve(declaration, extensionContext, arguments, invocationIndex, Optional.of(parameterContext)));
+	}
+
+	private Optional<? extends ResolvableParameterDeclaration> findDeclaration(int parameterIndex) {
+		ResolvableParameterDeclaration declaration = this.indexedParameterDeclarations.declarationsByIndex //
+				.get(parameterIndex);
+		if (declaration == null) {
+			return this.aggregatorParameters.stream() //
+					.filter(it -> it.getParameterIndex() == parameterIndex) //
+					.findFirst();
+		}
+		return Optional.of(declaration);
 	}
 
 	void resolveAndInjectFields(Object testInstance, ExtensionContext extensionContext, EvaluatedArgumentSet arguments,
@@ -254,7 +286,7 @@ private void setField(Object testInstance, FieldParameterDeclaration declaration
 			EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) {
 
 		Object argument = resolutionCache.resolve(declaration,
-			() -> resolve(declaration, extensionContext, arguments, invocationIndex));
+			() -> resolve(declaration, extensionContext, arguments, invocationIndex, Optional.empty()));
 		try {
 			declaration.getField().set(testInstance, argument);
 		}
@@ -263,17 +295,16 @@ private void setField(Object testInstance, FieldParameterDeclaration declaration
 		}
 	}
 
-	private Object resolve(FieldParameterDeclaration parameterDeclaration, ExtensionContext extensionContext,
-			EvaluatedArgumentSet arguments, int invocationIndex) {
-		return getResolver(extensionContext, parameterDeclaration, parameterDeclaration.getField()) //
-				.resolve(parameterDeclaration, arguments, invocationIndex);
+	private Object resolve(ResolvableParameterDeclaration parameterDeclaration, ExtensionContext extensionContext,
+			EvaluatedArgumentSet arguments, int invocationIndex, Optional<ParameterContext> parameterContext) {
+		Resolver resolver = getResolver(extensionContext, parameterDeclaration);
+		return parameterDeclaration.resolve(resolver, extensionContext, arguments, invocationIndex, parameterContext);
 	}
 
-	private Resolver getResolver(ExtensionContext extensionContext, ParameterDeclaration declaration,
-			AnnotatedElement annotatedElement) {
+	private Resolver getResolver(ExtensionContext extensionContext, ResolvableParameterDeclaration declaration) {
 		return this.resolvers.computeIfAbsent(declaration, __ -> this.aggregatorParameters.contains(declaration) //
-				? createAggregator(declaration.getParameterIndex(), annotatedElement, extensionContext) //
-				: createConverter(declaration.getParameterIndex(), annotatedElement, extensionContext));
+				? createAggregator(declaration, extensionContext) //
+				: createConverter(declaration, extensionContext));
 	}
 
 	private int toLogicalIndex(ParameterContext parameterContext) {
@@ -290,9 +321,46 @@ private static NavigableMap<Integer, FieldParameterDeclaration> validateFieldDec
 		validateIndexedParameters(indexedParameters, errors);
 		validateAggregatorParameters(aggregatorParameters, errors);
 
+		return configurationErrorOrSuccess(errors, () -> indexedParameters.entrySet().stream() //
+				.collect(toMap(Map.Entry::getKey, entry -> entry.getValue().get(0), (d, __) -> d, TreeMap::new)));
+	}
+
+	private static List<String> validateLifecycleMethodParameters(Method method, Annotation annotation,
+			ResolverFacade originalResolverFacade, ResolverFacade lifecycleMethodResolverFacade,
+			Map<ParameterDeclaration, ResolvableParameterDeclaration> parameterDeclarationMapping) {
+		List<ParameterDeclaration> actualDeclarations = lifecycleMethodResolverFacade.indexedParameterDeclarations.getAll();
+		List<String> errors = new ArrayList<>();
+		for (int parameterIndex = 0; parameterIndex < actualDeclarations.size(); parameterIndex++) {
+			ParameterDeclaration actualDeclaration = actualDeclarations.get(parameterIndex);
+			ResolvableParameterDeclaration originalDeclaration = originalResolverFacade.indexedParameterDeclarations.declarationsByIndex //
+					.get(parameterIndex);
+			if (originalDeclaration == null) {
+				break;
+			}
+			if (!actualDeclaration.getParameterType().equals(originalDeclaration.getParameterType())) {
+				errors.add(String.format(
+					"parameter%s with index %d is incompatible with the parameter declared on the parameterized class: expected type '%s' but found '%s'",
+					parameterName(actualDeclaration), parameterIndex, originalDeclaration.getParameterType(),
+					actualDeclaration.getParameterType()));
+			}
+			else if (findAnnotation(actualDeclaration.getAnnotatedElement(), ConvertWith.class).isPresent()) {
+				errors.add(String.format("parameter%s with index %d must not be annotated with @ConvertWith",
+					parameterName(actualDeclaration), parameterIndex));
+			}
+			else if (errors.isEmpty()) {
+				parameterDeclarationMapping.put(actualDeclaration, originalDeclaration);
+			}
+		}
+		return errors;
+	}
+
+	private static String parameterName(ParameterDeclaration actualDeclaration) {
+		return actualDeclaration.getParameterName().map(name -> " '" + name + "'").orElse("");
+	}
+
+	private static <T> T configurationErrorOrSuccess(List<String> errors, Supplier<T> successfulResult) {
 		if (errors.isEmpty()) {
-			return indexedParameters.entrySet().stream() //
-					.collect(toMap(Map.Entry::getKey, entry -> entry.getValue().get(0), (d, __) -> d, TreeMap::new));
+			return successfulResult.get();
 		}
 		else if (errors.size() == 1) {
 			throw new PreconditionViolationException("Configuration error: " + errors.get(0) + ".");
@@ -350,43 +418,31 @@ private static void validateAggregatorParameters(Set<FieldParameterDeclaration>
 				.forEach(errors::add);
 	}
 
-	/**
-	 * Determine if the supplied {@link Parameter} is an aggregator (i.e., of
-	 * type {@link ArgumentsAccessor} or annotated with {@link AggregateWith}).
-	 *
-	 * @return {@code true} if the parameter is an aggregator
-	 */
-	private static boolean isAggregator(ParameterDeclaration declaration) {
-		return ArgumentsAccessor.class.isAssignableFrom(declaration.getParameterType())
-				|| isAnnotated(declaration.getAnnotatedElement(), AggregateWith.class);
-	}
-
-	private static Converter createConverter(int index, AnnotatedElement annotatedElement,
-			ExtensionContext extensionContext) {
+	private static Converter createConverter(ParameterDeclaration declaration, ExtensionContext extensionContext) {
 		try { // @formatter:off
-			return findAnnotation(annotatedElement, ConvertWith.class)
+			return findAnnotation(declaration.getAnnotatedElement(), ConvertWith.class)
 					.map(ConvertWith::value)
 					.map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext))
-					.map(converter -> AnnotationConsumerInitializer.initialize(annotatedElement, converter))
+					.map(converter -> AnnotationConsumerInitializer.initialize(declaration.getAnnotatedElement(), converter))
 					.map(Converter::new)
 					.orElse(Converter.DEFAULT);
 		} // @formatter:on
 		catch (Exception ex) {
-			throw parameterResolutionException("Error creating ArgumentConverter", ex, index);
+			throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex());
 		}
 	}
 
-	private static Aggregator createAggregator(int index, AnnotatedElement annotatedElement,
-			ExtensionContext extensionContext) {
+	private static Aggregator createAggregator(ParameterDeclaration declaration, ExtensionContext extensionContext) {
 		try { // @formatter:off
-			return findAnnotation(annotatedElement, AggregateWith.class)
+			return findAnnotation(declaration.getAnnotatedElement(), AggregateWith.class)
 					.map(AggregateWith::value)
 					.map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsAggregator.class, clazz, extensionContext))
 					.map(Aggregator::new)
 					.orElse(Aggregator.DEFAULT);
 		} // @formatter:on
 		catch (Exception ex) {
-			throw parameterResolutionException("Error creating ArgumentsAggregator", ex, index);
+			throw parameterResolutionException("Error creating ArgumentsAggregator", ex,
+				declaration.getParameterIndex());
 		}
 	}
 
@@ -489,10 +545,10 @@ public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments,
 	private static class DefaultParameterDeclarations implements ParameterDeclarations {
 
 		private final AnnotatedElement sourceElement;
-		private final NavigableMap<Integer, ? extends ParameterDeclaration> declarationsByIndex;
+		private final NavigableMap<Integer, ? extends ResolvableParameterDeclaration> declarationsByIndex;
 
 		DefaultParameterDeclarations(AnnotatedElement sourceElement,
-				NavigableMap<Integer, ? extends ParameterDeclaration> declarationsByIndex) {
+				NavigableMap<Integer, ? extends ResolvableParameterDeclaration> declarationsByIndex) {
 			this.sourceElement = sourceElement;
 			this.declarationsByIndex = declarationsByIndex;
 		}
@@ -538,4 +594,176 @@ static String describe(AnnotatedElement sourceElement) {
 		}
 	}
 
+	private abstract static class ResolvableParameterDeclaration implements ParameterDeclaration {
+
+		/**
+		 * Determine if the supplied {@link Parameter} is an aggregator (i.e., of
+		 * type {@link ArgumentsAccessor} or annotated with {@link AggregateWith}).
+		 *
+		 * @return {@code true} if the parameter is an aggregator
+		 */
+		boolean isAggregator() {
+			return ArgumentsAccessor.class.isAssignableFrom(getParameterType())
+					|| isAnnotated(getAnnotatedElement(), AggregateWith.class);
+		}
+
+		protected abstract Object resolve(Resolver resolver, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex,
+				Optional<ParameterContext> originalParameterContext);
+	}
+
+	private static class FieldParameterDeclaration extends ResolvableParameterDeclaration implements FieldContext {
+
+		private final Field field;
+		private final int index;
+
+		FieldParameterDeclaration(Field field, int index) {
+			this.field = field;
+			this.index = index;
+		}
+
+		@Override
+		public Field getField() {
+			return this.field;
+		}
+
+		@Override
+		public Field getAnnotatedElement() {
+			return this.field;
+		}
+
+		@Override
+		public Class<?> getParameterType() {
+			return this.field.getType();
+		}
+
+		@Override
+		public int getParameterIndex() {
+			return index;
+		}
+
+		@Override
+		public Optional<String> getParameterName() {
+			return Optional.of(this.field.getName());
+		}
+
+		@Override
+		public Object resolve(Resolver resolver, ExtensionContext extensionContext, EvaluatedArgumentSet arguments,
+				int invocationIndex, Optional<ParameterContext> originalParameterContext) {
+			return resolver.resolve(this, arguments, invocationIndex);
+		}
+	}
+
+	private static class ExecutableParameterDeclaration extends ResolvableParameterDeclaration {
+
+		private final java.lang.reflect.Parameter parameter;
+		private final int index;
+		private final int indexOffset;
+
+		ExecutableParameterDeclaration(java.lang.reflect.Parameter parameter, int index, int indexOffset) {
+			this.parameter = parameter;
+			this.index = index;
+			this.indexOffset = indexOffset;
+		}
+
+		@Override
+		public java.lang.reflect.Parameter getAnnotatedElement() {
+			return this.parameter;
+		}
+
+		@Override
+		public Class<?> getParameterType() {
+			return this.parameter.getType();
+		}
+
+		@Override
+		public int getParameterIndex() {
+			return this.index - this.indexOffset;
+		}
+
+		@Override
+		public Optional<String> getParameterName() {
+			return this.parameter.isNamePresent() ? Optional.of(this.parameter.getName()) : Optional.empty();
+		}
+
+		@Override
+		public Object resolve(Resolver resolver, ExtensionContext extensionContext, EvaluatedArgumentSet arguments,
+				int invocationIndex, Optional<ParameterContext> originalParameterContext) {
+			ParameterContext parameterContext = originalParameterContext //
+					.filter(it -> it.getParameter().equals(this.parameter)) //
+					.orElseGet(() -> toParameterContext(extensionContext, originalParameterContext));
+			return resolver.resolve(parameterContext, getParameterIndex(), arguments, invocationIndex);
+		}
+
+		private ParameterContext toParameterContext(ExtensionContext extensionContext,
+				Optional<ParameterContext> originalParameterContext) {
+			Optional<Object> target = originalParameterContext.flatMap(ParameterContext::getTarget);
+			if (!target.isPresent()) {
+				target = extensionContext.getTestInstance();
+			}
+			return toParameterContext(target);
+		}
+
+		private ParameterContext toParameterContext(Optional<Object> target) {
+			return new ParameterContext() {
+				@Override
+				public java.lang.reflect.Parameter getParameter() {
+					return ExecutableParameterDeclaration.this.parameter;
+				}
+
+				@Override
+				public int getIndex() {
+					return ExecutableParameterDeclaration.this.index;
+				}
+
+				@Override
+				public Optional<Object> getTarget() {
+					return target;
+				}
+			};
+		}
+	}
+
+	private static class DefaultArgumentSetLifecycleMethodParameterResolver
+			implements ArgumentSetLifecycleMethod.ParameterResolver {
+
+		private final ResolverFacade originalResolverFacade;
+		private final ResolverFacade lifecycleMethodResolverFacade;
+		private final Map<ParameterDeclaration, ResolvableParameterDeclaration> parameterDeclarationMapping;
+
+		DefaultArgumentSetLifecycleMethodParameterResolver(ResolverFacade originalResolverFacade,
+				ResolverFacade lifecycleMethodResolverFacade,
+				Map<ParameterDeclaration, ResolvableParameterDeclaration> parameterDeclarationMapping) {
+			this.originalResolverFacade = originalResolverFacade;
+			this.lifecycleMethodResolverFacade = lifecycleMethodResolverFacade;
+			this.parameterDeclarationMapping = parameterDeclarationMapping;
+		}
+
+		@Override
+		public boolean supports(ParameterContext parameterContext) {
+			return this.lifecycleMethodResolverFacade.findDeclaration(parameterContext.getIndex()) //
+					.filter(it -> this.parameterDeclarationMapping.containsKey(it) || it.isAggregator()) //
+					.isPresent();
+		}
+
+		@Override
+		public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) {
+
+			ResolvableParameterDeclaration actualDeclaration = this.lifecycleMethodResolverFacade //
+					.findDeclaration(parameterContext.getIndex()) //
+					.orElseThrow(() -> new ParameterResolutionException(
+						"Parameter index out of bounds: " + parameterContext.getIndex()));
+
+			ResolvableParameterDeclaration originalDeclaration = this.parameterDeclarationMapping //
+					.get(actualDeclaration);
+			if (originalDeclaration == null) {
+				return this.lifecycleMethodResolverFacade.resolve(actualDeclaration, extensionContext, arguments,
+					invocationIndex, Optional.of(parameterContext));
+			}
+			return resolutionCache.resolve(originalDeclaration,
+				() -> this.originalResolverFacade.resolve(originalDeclaration, extensionContext, arguments,
+					invocationIndex, Optional.of(parameterContext)));
+		}
+	}
 }


Test patch:
diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
index 68b70ee7ee26..781de6b90c74 100644
--- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
+++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
@@ -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.
@@ -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
 ====
@@ -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
diff --git a/documentation/src/test/java/example/ParameterizedMigrationDemo.java b/documentation/src/test/java/example/ParameterizedMigrationDemo.java
new file mode 100644
index 000000000000..72bbd3378c48
--- /dev/null
+++ b/documentation/src/test/java/example/ParameterizedMigrationDemo.java
@@ -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[]
+
+}
diff --git a/documentation/src/test/java21/example/ParameterizedLifecycleDemo.java b/documentation/src/test/java21/example/ParameterizedLifecycleDemo.java
new file mode 100644
index 000000000000..50cd7e837392
--- /dev/null
+++ b/documentation/src/test/java21/example/ParameterizedLifecycleDemo.java
@@ -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[]
+
+}
diff --git a/documentation/src/test/java21/example/ParameterizedRecordDemo.java b/documentation/src/test/java21/example/ParameterizedRecordDemo.java
index beb62b6bc81e..2e974207046d 100644
--- a/documentation/src/test/java21/example/ParameterizedRecordDemo.java
+++ b/documentation/src/test/java21/example/ParameterizedRecordDemo.java
@@ -14,7 +14,6 @@
 
 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;
@@ -22,7 +21,6 @@
 public class ParameterizedRecordDemo {
 
 	@SuppressWarnings("JUnitMalformedDeclaration")
-	@Nested
 	// tag::example[]
 	@ParameterizedClass
 	@CsvSource({ "apple, 23", "banana, 42" })
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java
index a20cf89e01c5..b50b63b68308 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java
@@ -39,6 +39,7 @@
 import static org.junit.platform.testkit.engine.EventConditions.test;
 import static org.junit.platform.testkit.engine.EventConditions.uniqueId;
 import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -46,11 +47,15 @@
 import java.lang.annotation.Target;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import org.assertj.core.api.Condition;
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Named;
 import org.junit.jupiter.api.Nested;
@@ -60,6 +65,7 @@
 import org.junit.jupiter.api.TestReporter;
 import org.junit.jupiter.api.extension.AnnotatedElementContext;
 import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
 import org.junit.jupiter.engine.Constants;
 import org.junit.jupiter.engine.descriptor.ContainerTemplateInvocationTestDescriptor;
@@ -68,6 +74,7 @@
 import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
 import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator;
 import org.junit.jupiter.params.converter.ArgumentConversionException;
+import org.junit.jupiter.params.converter.ArgumentConverter;
 import org.junit.jupiter.params.converter.ConvertWith;
 import org.junit.jupiter.params.converter.SimpleArgumentConverter;
 import org.junit.jupiter.params.converter.TypedArgumentConverter;
@@ -81,13 +88,16 @@
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.NullAndEmptySource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.jupiter.params.support.FieldContext;
 import org.junit.jupiter.params.support.ParameterDeclarations;
 import org.junit.platform.commons.util.StringUtils;
 import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestExecutionResult;
 import org.junit.platform.engine.UniqueId;
 import org.junit.platform.engine.reporting.ReportEntry;
 import org.junit.platform.testkit.engine.EngineExecutionResults;
 import org.junit.platform.testkit.engine.Event;
+import org.junit.platform.testkit.engine.Events;
 
 @SuppressWarnings("JUnitMalformedDeclaration")
 public class ParameterizedClassIntegrationTests extends AbstractJupiterTestEngineTests {
@@ -99,7 +109,7 @@ public class ParameterizedClassIntegrationTests extends AbstractJupiterTestEngin
 			FieldInjectionWithRegisteredConversionTestCase.class, RecordWithBuiltInAggregatorTestCase.class,
 			FieldInjectionWithBuiltInAggregatorTestCase.class, RecordWithCustomAggregatorTestCase.class,
 			FieldInjectionWithCustomAggregatorTestCase.class })
-	void injectsParametersIntoContainerTemplate(Class<?> containerTemplateClass) {
+	void injectsParametersIntoClass(Class<?> containerTemplateClass) {
 
 		var results = executeTestsForClass(containerTemplateClass);
 
@@ -134,333 +144,575 @@ void injectsParametersIntoContainerTemplate(Class<?> containerTemplateClass) {
 	}
 
 	@ParameterizedTest
-	@ValueSource(classes = { //NullAndEmptySourceConstructorInjectionTestCase.class,
-			NullAndEmptySourceConstructorFieldInjectionTestCase.class })
-	void supportsNullAndEmptySource(Class<?> containerTemplateClass) {
+	@ValueSource(classes = { ArgumentConversionPerInvocationConstructorInjectionTestCase.class,
+			ArgumentConversionPerInvocationFieldInjectionTestCase.class })
+	void argumentConverterIsOnlyCalledOncePerInvocation(Class<?> containerTemplateClass) {
 
 		var results = executeTestsForClass(containerTemplateClass);
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] value=null", "[2] value=");
+		results.allEvents().assertStatistics(stats -> stats.started(5).succeeded(5));
 	}
 
-	@ParameterizedTest
-	@ValueSource(classes = { CsvFileSourceConstructorInjectionTestCase.class,
-			CsvFileSourceFieldInjectionTestCase.class })
-	void supportsCsvFileSource(Class<?> containerTemplateClass) {
+	@Nested
+	class Sources {
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@ParameterizedTest
+		@ValueSource(classes = { NullAndEmptySourceConstructorInjectionTestCase.class,
+				NullAndEmptySourceConstructorFieldInjectionTestCase.class })
+		void supportsNullAndEmptySource(Class<?> containerTemplateClass) {
 
-		results.allEvents().assertStatistics(stats -> stats.started(10).succeeded(10));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] name=foo, value=1", "[2] name=bar, value=2", "[3] name=baz, value=3",
-					"[4] name=qux, value=4");
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@ParameterizedTest
-	@ValueSource(classes = { SingleEnumSourceConstructorInjectionTestCase.class,
-			SingleEnumSourceFieldInjectionTestCase.class })
-	void supportsSingleEnumSource(Class<?> containerTemplateClass) {
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] value=null", "[2] value=");
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@ParameterizedTest
+		@ValueSource(classes = { CsvFileSourceConstructorInjectionTestCase.class,
+				CsvFileSourceFieldInjectionTestCase.class })
+		void supportsCsvFileSource(Class<?> containerTemplateClass) {
 
-		results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] value=FOO");
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@ParameterizedTest
-	@ValueSource(classes = { RepeatedEnumSourceConstructorInjectionTestCase.class,
-			RepeatedEnumSourceFieldInjectionTestCase.class })
-	void supportsRepeatedEnumSource(Class<?> containerTemplateClass) {
+			results.allEvents().assertStatistics(stats -> stats.started(10).succeeded(10));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] name=foo, value=1", "[2] name=bar, value=2", "[3] name=baz, value=3",
+						"[4] name=qux, value=4");
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@ParameterizedTest
+		@ValueSource(classes = { SingleEnumSourceConstructorInjectionTestCase.class,
+				SingleEnumSourceFieldInjectionTestCase.class })
+		void supportsSingleEnumSource(Class<?> containerTemplateClass) {
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] value=FOO", "[2] value=BAR");
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@ParameterizedTest
-	@ValueSource(classes = { MethodSourceConstructorInjectionTestCase.class, MethodSourceFieldInjectionTestCase.class })
-	void supportsMethodSource(Class<?> containerTemplateClass) {
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] value=FOO");
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@ParameterizedTest
+		@ValueSource(classes = { RepeatedEnumSourceConstructorInjectionTestCase.class,
+				RepeatedEnumSourceFieldInjectionTestCase.class })
+		void supportsRepeatedEnumSource(Class<?> containerTemplateClass) {
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] value=foo", "[2] value=bar");
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@Test
-	void doesNotSupportDerivingMethodName() {
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] value=FOO", "[2] value=BAR");
+		}
 
-		var results = executeTestsForClass(MethodSourceWithoutMethodNameTestCase.class);
+		@ParameterizedTest
+		@ValueSource(classes = { MethodSourceConstructorInjectionTestCase.class,
+				MethodSourceFieldInjectionTestCase.class })
+		void supportsMethodSource(Class<?> containerTemplateClass) {
 
-		results.allEvents().failed() //
-				.assertEventsMatchExactly(finishedWithFailure(
-					message("You must specify a method name when using @MethodSource with @ContainerTemplate")));
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@ParameterizedTest
-	@ValueSource(classes = { FieldSourceConstructorInjectionTestCase.class, FieldSourceFieldInjectionTestCase.class })
-	void supportsFieldSource(Class<?> containerTemplateClass) {
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] value=foo", "[2] value=bar");
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@Test
+		void doesNotSupportDerivingMethodName() {
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] value=foo", "[2] value=bar");
-	}
+			var results = executeTestsForClass(MethodSourceWithoutMethodNameTestCase.class);
 
-	@Test
-	void doesNotSupportDerivingFieldName() {
+			results.allEvents().failed() //
+					.assertEventsMatchExactly(finishedWithFailure(
+						message("You must specify a method name when using @MethodSource with @ContainerTemplate")));
+		}
 
-		var results = executeTestsForClass(FieldSourceWithoutFieldNameTestCase.class);
+		@ParameterizedTest
+		@ValueSource(classes = { FieldSourceConstructorInjectionTestCase.class,
+				FieldSourceFieldInjectionTestCase.class })
+		void supportsFieldSource(Class<?> containerTemplateClass) {
 
-		results.allEvents().failed() //
-				.assertEventsMatchExactly(finishedWithFailure(
-					message("You must specify a field name when using @FieldSource with @ContainerTemplate")));
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@ParameterizedTest
-	@ValueSource(classes = { ArgumentsSourceConstructorInjectionTestCase.class,
-			ArgumentsSourceFieldInjectionTestCase.class })
-	void supportsArgumentsSource(Class<?> containerTemplateClass) {
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] value=foo", "[2] value=bar");
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@Test
+		void doesNotSupportDerivingFieldName() {
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("[1] value=foo", "[2] value=bar");
-	}
+			var results = executeTestsForClass(FieldSourceWithoutFieldNameTestCase.class);
 
-	@Test
-	void supportsCustomNamePatterns() {
+			results.allEvents().failed() //
+					.assertEventsMatchExactly(finishedWithFailure(
+						message("You must specify a field name when using @FieldSource with @ContainerTemplate")));
+		}
 
-		var results = executeTestsForClass(CustomNamePatternTestCase.class);
+		@ParameterizedTest
+		@ValueSource(classes = { ArgumentsSourceConstructorInjectionTestCase.class,
+				ArgumentsSourceFieldInjectionTestCase.class })
+		void supportsArgumentsSource(Class<?> containerTemplateClass) {
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly("1 | TesT | 1, foo | set", "2 | TesT | 2, bar | number=2, name=bar");
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@Test
-	void closesAutoCloseableArguments() {
-		AutoCloseableArgument.closeCounter = 0;
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("[1] value=foo", "[2] value=bar");
+		}
 
-		var results = executeTestsForClass(AutoCloseableArgumentTestCase.class);
+		@Test
+		void failsWhenNoArgumentsSourceIsDeclared() {
+			var results = executeTestsForClass(NoArgumentSourceTestCase.class);
 
-		results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
-		assertThat(AutoCloseableArgument.closeCounter).isEqualTo(2);
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, event(finishedWithFailure(message(
+						"Configuration error: You must configure at least one arguments source for this @ParameterizedClass"))));
+		}
 	}
 
-	@Test
-	void doesNotCloseAutoCloseableArgumentsWhenDisabled() {
-		AutoCloseableArgument.closeCounter = 0;
+	@Nested
+	class AnnotationAttributes {
 
-		var results = executeTestsForClass(AutoCloseableArgumentWithDisabledCleanupTestCase.class);
+		@Test
+		void supportsCustomNamePatterns() {
 
-		results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
-		assertThat(AutoCloseableArgument.closeCounter).isEqualTo(0);
-	}
+			var results = executeTestsForClass(CustomNamePatternTestCase.class);
 
-	@Test
-	void failsOnStrictArgumentCountValidationMode() {
-		var results = executeTestsForClass(StrictArgumentCountValidationModeTestCase.class);
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly("1 | TesT | 1, foo | set", "2 | TesT | 2, bar | number=2, name=bar");
+		}
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, event(finishedWithFailure(message(
-					"Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted()))));
-	}
+		@Test
+		void closesAutoCloseableArguments() {
+			AutoCloseableArgument.closeCounter = 0;
 
-	@ParameterizedTest
-	@ValueSource(classes = { NoneArgumentCountValidationModeTestCase.class,
-			DefaultArgumentCountValidationModeTestCase.class })
-	void doesNotFailOnNoneOrDefaultArgumentCountValidationMode(Class<?> containerTemplateClass) {
+			var results = executeTestsForClass(AutoCloseableArgumentTestCase.class);
 
-		var results = executeTestsForClass(containerTemplateClass);
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
+			assertThat(AutoCloseableArgument.closeCounter).isEqualTo(2);
+		}
 
-		results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
-	}
+		@Test
+		void doesNotCloseAutoCloseableArgumentsWhenDisabled() {
+			AutoCloseableArgument.closeCounter = 0;
 
-	@Test
-	void failsOnStrictArgumentCountValidationModeSetViaConfigurationParameter() {
-		var results = executeTests(request -> request //
-				.selectors(selectClass(DefaultArgumentCountValidationModeTestCase.class)).configurationParameter(
-					ArgumentCountValidator.ARGUMENT_COUNT_VALIDATION_KEY, STRICT.name()));
+			var results = executeTestsForClass(AutoCloseableArgumentWithDisabledCleanupTestCase.class);
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, event(finishedWithFailure(message(
-					"Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted()))));
-	}
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
+			assertThat(AutoCloseableArgument.closeCounter).isEqualTo(0);
+		}
 
-	@Test
-	void failsForSkippedParameters() {
-		var results = executeTestsForClass(InvalidUnusedParameterIndexesTestCase.class);
+		@Test
+		void failsOnStrictArgumentCountValidationMode() {
+			var results = executeTestsForClass(StrictArgumentCountValidationModeTestCase.class);
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, event(finishedWithFailure(message(
-					"2 configuration errors:%n- no field annotated with @Parameter(0) declared%n- no field annotated with @Parameter(2) declared".formatted()))));
-	}
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, event(finishedWithFailure(message(
+						"Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted()))));
+		}
 
-	@Test
-	void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() {
-		var results = executeTestsForClass(ForbiddenZeroInvocationsTestCase.class);
+		@ParameterizedTest
+		@ValueSource(classes = { NoneArgumentCountValidationModeTestCase.class,
+				DefaultArgumentCountValidationModeTestCase.class })
+		void doesNotFailOnNoneOrDefaultArgumentCountValidationMode(Class<?> containerTemplateClass) {
 
-		results.containerEvents().assertThatEvents() //
-				.haveExactly(1, event(finishedWithFailure(message(
-					"Configuration error: You must configure at least one set of arguments for this @ParameterizedClass"))));
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@Test
-	void doesNotFailWhenInvocationIsNotRequiredAndNoArgumentSetsAreProvided() {
-		var results = executeTestsForClass(AllowedZeroInvocationsTestCase.class);
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
+		}
 
-		results.allEvents().assertStatistics(stats -> stats.started(2).succeeded(2));
-	}
+		@Test
+		void failsOnStrictArgumentCountValidationModeSetViaConfigurationParameter() {
+			var results = executeTests(request -> request //
+					.selectors(selectClass(DefaultArgumentCountValidationModeTestCase.class)).configurationParameter(
+						ArgumentCountValidator.ARGUMENT_COUNT_VALIDATION_KEY, STRICT.name()));
 
-	@Test
-	void failsWhenNoArgumentsSourceIsDeclared() {
-		var results = executeTestsForClass(NoArgumentSourceTestCase.class);
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, event(finishedWithFailure(message(
+						"Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted()))));
+		}
 
-		results.containerEvents().assertThatEvents() //
-				.haveExactly(1, event(finishedWithFailure(message(
-					"Configuration error: You must configure at least one arguments source for this @ParameterizedClass"))));
-	}
+		@Test
+		void failsForSkippedParameters() {
+			var results = executeTestsForClass(InvalidUnusedParameterIndexesTestCase.class);
 
-	@ParameterizedTest
-	@ValueSource(classes = { NestedFieldInjectionTestCase.class, NestedConstructorInjectionTestCase.class })
-	void supportsNestedParameterizedClasss(Class<?> containerTemplateClass) {
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, event(finishedWithFailure(message(
+						"2 configuration errors:%n- no field annotated with @Parameter(0) declared%n- no field annotated with @Parameter(2) declared".formatted()))));
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@Test
+		void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() {
+			var results = executeTestsForClass(ForbiddenZeroInvocationsTestCase.class);
 
-		results.containerEvents().assertStatistics(stats -> stats.started(14).succeeded(14));
-		results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8));
-		assertThat(invocationDisplayNames(results)) //
-				.containsExactly( //
-					"[1] number=1", "[1] text=foo", "[2] text=bar", //
-					"[2] number=2", "[1] text=foo", "[2] text=bar" //
-				);
-	}
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, event(finishedWithFailure(message(
+						"Configuration error: You must configure at least one set of arguments for this @ParameterizedClass"))));
+		}
 
-	@ParameterizedTest
-	@ValueSource(classes = { ConstructorInjectionWithRegularNestedTestCase.class,
-			FieldInjectionWithRegularNestedTestCase.class })
-	void supportsRegularNestedTestClassesInsideParameterizedClass(Class<?> containerTemplateClass) {
+		@Test
+		void doesNotFailWhenInvocationIsNotRequiredAndNoArgumentSetsAreProvided() {
+			var results = executeTestsForClass(AllowedZeroInvocationsTestCase.class);
+
+			results.allEvents().assertStatistics(stats -> stats.started(2).succeeded(2));
+		}
+	}
+
+	@Nested
+	class Nesting {
+
+		@ParameterizedTest
+		@ValueSource(classes = { //NestedFieldInjectionTestCase.class,
+				NestedConstructorInjectionTestCase.class })
+		void supportsNestedParameterizedClass(Class<?> containerTemplateClass) {
+
+			var results = executeTestsForClass(containerTemplateClass);
+
+			results.containerEvents().assertStatistics(stats -> stats.started(14).succeeded(14));
+			results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8));
+			assertThat(invocationDisplayNames(results)) //
+					.containsExactly( //
+						"[1] number=1", "[1] text=foo", "[2] text=bar", //
+						"[2] number=2", "[1] text=foo", "[2] text=bar" //
+					);
+			assertThat(allReportEntries(results)).map(it -> it.get("value")).containsExactly(
+			// @formatter:off
+					"beforeAll: %s".formatted(containerTemplateClass.getSimpleName()),
+					"beforeArgumentSet: %s".formatted(containerTemplateClass.getSimpleName()),
+					"beforeAll: InnerTestCase",
+					"beforeArgumentSet: InnerTestCase",
+					"beforeEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [1] flag=true [InnerTestCase]",
+					"test(1, foo, true)",
+					"afterEach: [1] flag=true [InnerTestCase]",
+					"afterEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [InnerTestCase]",
+					"test(1, foo, false)",
+					"afterEach: [2] flag=false [InnerTestCase]",
+					"afterEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"afterArgumentSet: InnerTestCase",
+					"beforeArgumentSet: InnerTestCase",
+					"beforeEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [1] flag=true [InnerTestCase]",
+					"test(1, bar, true)",
+					"afterEach: [1] flag=true [InnerTestCase]",
+					"afterEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [InnerTestCase]",
+					"test(1, bar, false)",
+					"afterEach: [2] flag=false [InnerTestCase]",
+					"afterEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"afterArgumentSet: InnerTestCase",
+					"afterAll: InnerTestCase",
+					"afterArgumentSet: %s".formatted(containerTemplateClass.getSimpleName()),
+					"beforeArgumentSet: %s".formatted(containerTemplateClass.getSimpleName()),
+					"beforeAll: InnerTestCase",
+					"beforeArgumentSet: InnerTestCase",
+					"beforeEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [1] flag=true [InnerTestCase]",
+					"test(2, foo, true)",
+					"afterEach: [1] flag=true [InnerTestCase]",
+					"afterEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [InnerTestCase]",
+					"test(2, foo, false)",
+					"afterEach: [2] flag=false [InnerTestCase]",
+					"afterEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"afterArgumentSet: InnerTestCase",
+					"beforeArgumentSet: InnerTestCase",
+					"beforeEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [1] flag=true [InnerTestCase]",
+					"test(2, bar, true)",
+					"afterEach: [1] flag=true [InnerTestCase]",
+					"afterEach: [1] flag=true [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"beforeEach: [2] flag=false [InnerTestCase]",
+					"test(2, bar, false)",
+					"afterEach: [2] flag=false [InnerTestCase]",
+					"afterEach: [2] flag=false [%s]".formatted(containerTemplateClass.getSimpleName()),
+					"afterArgumentSet: InnerTestCase",
+					"afterAll: InnerTestCase",
+					"afterArgumentSet: %s".formatted(containerTemplateClass.getSimpleName()),
+					"afterAll: %s".formatted(containerTemplateClass.getSimpleName())
+					// @formatter:on
+			);
+		}
+
+		@ParameterizedTest
+		@ValueSource(classes = { ConstructorInjectionWithRegularNestedTestCase.class,
+				FieldInjectionWithRegularNestedTestCase.class })
+		void supportsRegularNestedTestClassesInsideParameterizedClass(Class<?> containerTemplateClass) {
+
+			var results = executeTestsForClass(containerTemplateClass);
+
+			results.containerEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+			results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2));
+		}
+	}
+
+	@Nested
+	class FieldInjection {
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@Test
+		void supportsMultipleAggregatorFields() {
 
-		results.containerEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-		results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2));
-	}
+			var results = executeTestsForClass(MultiAggregatorFieldInjectionTestCase.class);
+
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+		}
 
-	@Test
-	void supportsMultipleAggregatorFields() {
+		@Test
+		void supportsInjectionOfInheritedFields() {
 
-		var results = executeTestsForClass(MultiAggregatorFieldInjectionTestCase.class);
+			var results = executeTestsForClass(InheritedHiddenParameterFieldTestCase.class);
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
-	}
+			results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
 
-	@Test
-	void supportsFieldInjectionForTestInstanceLifecyclePerClass() {
+			assertThat(allReportEntries(results)) //
+					.extracting(it -> tuple(it.get("super.value"), it.get("this.value"))) //
+					.containsExactly(tuple("foo", "1"), tuple("bar", "2"));
+		}
 
-		var results = executeTestsForClass(FieldInjectionWithPerClassTestInstanceLifecycleTestCase.class);
+		@Test
+		void doesNotSupportInjectionForFinalFields() {
 
-		results.allEvents().assertStatistics(stats -> stats.started(8).succeeded(8));
+			var containerTemplateClass = InvalidFinalFieldTestCase.class;
 
-		assertThat(allReportEntries(results).map(it -> it.get("value"))) //
-				.containsExactly("foo", "foo", "bar", "bar");
-		assertThat(allReportEntries(results).map(it -> it.get("instanceHashCode")).distinct()) //
-				.hasSize(1);
-	}
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@Test
-	void doesNotSupportConstructorInjectionForTestInstanceLifecyclePerClass() {
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						"Configuration error: @Parameter field [final int %s.i] must not be declared as final.".formatted(
+							containerTemplateClass.getName()))));
+		}
 
-		var results = executeTests(request -> request //
-				.selectors(selectClass(ConstructorInjectionTestCase.class)) //
-				.configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, PER_CLASS.name()));
+		@Test
+		void aggregatorFieldsMustNotDeclareIndex() {
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, finishedWithFailure(message(it -> it.contains(
-					"Constructor injection is not supported for @ParameterizedClass classes with @TestInstance(Lifecycle.PER_CLASS)"))));
-	}
+			var containerTemplateClass = InvalidAggregatorFieldWithIndexTestCase.class;
 
-	@Test
-	void supportsInjectionOfInheritedFields() {
+			var results = executeTestsForClass(containerTemplateClass);
 
-		var results = executeTestsForClass(InheritedHiddenParameterFieldTestCase.class);
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						"Configuration error: no index may be declared in @Parameter(0) annotation on aggregator field [%s %s.accessor].".formatted(
+							ArgumentsAccessor.class.getName(), containerTemplateClass.getName()))));
+		}
 
-		results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6));
+		@Test
+		void declaredIndexMustNotBeNegative() {
 
-		assertThat(allReportEntries(results)) //
-				.extracting(it -> tuple(it.get("super.value"), it.get("this.value"))) //
-				.containsExactly(tuple("foo", "1"), tuple("bar", "2"));
-	}
+			var containerTemplateClass = InvalidParameterIndexTestCase.class;
+
+			var results = executeTestsForClass(containerTemplateClass);
 
-	@Test
-	void doesNotSupportInjectionForFinalFields() {
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						"Configuration error: index must be greater than or equal to zero in @Parameter(-42) annotation on field [int %s.i].".formatted(
+							containerTemplateClass.getName()))));
+		}
 
-		var containerTemplateClass = InvalidFinalFieldTestCase.class;
+		@Test
+		void declaredIndexMustBeUnique() {
 
-		var results = executeTestsForClass(containerTemplateClass);
+			var containerTemplateClass = InvalidDuplicateParameterDeclarationTestCase.class;
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, finishedWithFailure(message(
-					"Configuration error: @Parameter field [final int %s.i] must not be declared as final.".formatted(
-						containerTemplateClass.getName()))));
+			var results = executeTestsForClass(containerTemplateClass);
+
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						"Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted(
+							containerTemplateClass.getName(), containerTemplateClass.getName()))));
+		}
 	}
 
-	@Test
-	void aggregatorFieldsMustNotDeclareIndex() {
+	@Nested
+	class PerClassLifecycle {
 
-		var containerTemplateClass = InvalidAggregatorFieldWithIndexTestCase.class;
+		@Test
+		void supportsFieldInjectionForTestInstanceLifecyclePerClass() {
+
+			var results = executeTestsForClass(FieldInjectionWithPerClassTestInstanceLifecycleTestCase.class);
+
+			results.allEvents().assertStatistics(stats -> stats.started(8).succeeded(8));
+
+			Supplier<Stream<Map<String, String>>> valueTrackingReportEntries = () -> allReportEntries(results) //
+					.filter(it -> it.containsKey("instanceHashCode"));
+			Supplier<Stream<Map<String, String>>> lifecycleReportEntries = () -> allReportEntries(results) //
+					.filter(it -> !it.containsKey("instanceHashCode"));
+
+			assertThat(valueTrackingReportEntries.get().map(it -> it.get("value"))) //
+					.containsExactly("foo", "foo", "bar", "bar");
+			assertThat(valueTrackingReportEntries.get().map(it -> it.get("instanceHashCode")).distinct()) //
+					.hasSize(1);
+			assertThat(lifecycleReportEntries.get().map(it -> it.get("value"))) //
+					.containsExactly(
+					//@formatter:off
+							"beforeArgumentSet1",
+							"beforeArgumentSet2",
+							"test1",
+							"test2",
+							"afterArgumentSet1",
+							"afterArgumentSet2",
+							"beforeArgumentSet1",
+							"beforeArgumentSet2",
+							"test1",
+							"test2",
+							"afterArgumentSet1",
+							"afterArgumentSet2"
+							//@formatter:on
+					);
+		}
 
-		var results = executeTestsForClass(containerTemplateClass);
+		@Test
+		void doesNotSupportConstructorInjectionForTestInstanceLifecyclePerClass() {
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, finishedWithFailure(message(
-					"Configuration error: no index may be declared in @Parameter(0) annotation on aggregator field [%s %s.accessor].".formatted(
-						ArgumentsAccessor.class.getName(), containerTemplateClass.getName()))));
+			var results = executeTests(request -> request //
+					.selectors(selectClass(ConstructorInjectionTestCase.class)) //
+					.configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, PER_CLASS.name()));
+
+			results.allEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(it -> it.contains(
+						"Constructor injection is not supported for @ParameterizedClass classes with @TestInstance(Lifecycle.PER_CLASS)"))));
+		}
 	}
 
-	@Test
-	void declaredIndexMustNotBeNegative() {
+	@Nested
+	class LifecycleMethods {
 
-		var containerTemplateClass = InvalidParameterIndexTestCase.class;
+		@ParameterizedTest
+		@CsvSource(textBlock = """
+				NonStaticBeforeLifecycleMethodTestCase, @BeforeArgumentSet, beforeArgumentSet
+				NonStaticAfterLifecycleMethodTestCase,  @AfterArgumentSet,  afterArgumentSet
+				""")
+		void lifecycleMethodsNeedToBeStaticByDefault(String simpleClassName, String annotationName,
+				String lifecycleMethodName) throws Exception {
 
-		var results = executeTestsForClass(containerTemplateClass);
+			var className = ParameterizedClassIntegrationTests.class.getName() + "$" + simpleClassName;
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, finishedWithFailure(message(
-					"Configuration error: index must be greater than or equal to zero in @Parameter(-42) annotation on field [int %s.i].".formatted(
-						containerTemplateClass.getName()))));
-	}
+			var results = executeTestsForClass(Class.forName(className));
+
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						"%s method 'void %s.%s()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS)." //
+								.formatted(annotationName, className, lifecycleMethodName))));
+		}
 
-	@Test
-	void declaredIndexMustBeUnique() {
+		@Test
+		void lifecycleMethodsMustNotBePrivate() {
 
-		var containerTemplateClass = InvalidDuplicateParameterDeclarationTestCase.class;
+			var results = executeTestsForClass(PrivateLifecycleMethodTestCase.class);
 
-		var results = executeTestsForClass(containerTemplateClass);
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						"@BeforeArgumentSet method 'private static void %s.beforeArgumentSet()' must not be private." //
+								.formatted(PrivateLifecycleMethodTestCase.class.getName()))));
+		}
 
-		results.allEvents().assertThatEvents() //
-				.haveExactly(1, finishedWithFailure(message(
-					"Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted(
-						containerTemplateClass.getName(), containerTemplateClass.getName()))));
-	}
+		@Test
+		void lifecycleMethodsFromSuperclassAreWrappedAroundLifecycleMethodsFromTestClass() {
 
-	@ParameterizedTest
-	@ValueSource(classes = { ArgumentConversionPerInvocationConstructorInjectionTestCase.class,
-			ArgumentConversionPerInvocationFieldInjectionTestCase.class })
-	void argumentConverterIsOnlyCalledOncePerInvocation(Class<?> containerTemplateClass) {
+			var results = executeTestsForClass(LifecycleMethodsFromSuperclassTestCase.class);
 
-		var results = executeTestsForClass(containerTemplateClass);
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
 
-		results.allEvents().assertStatistics(stats -> stats.started(5).succeeded(5));
+			assertThat(allReportEntries(results).map(it -> it.get("value"))) //
+					.containsExactly("zzz_before", "aaa_before", "test", "aaa_after", "zzz_after");
+		}
+
+		@Test
+		void exceptionsInLifecycleMethodsArePropagated() {
+
+			var results = executeTestsForClass(LifecycleMethodsErrorHandlingTestCase.class);
+
+			results.allEvents().assertStatistics(stats -> stats.started(3).failed(1).succeeded(2));
+
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure( //
+						message("zzz_before"), //
+						suppressed(0, message("aaa_after")), //
+						suppressed(1, message("zzz_after"))));
+
+			assertThat(allReportEntries(results).map(it -> it.get("value"))) //
+					.containsExactly("zzz_before", "aaa_after", "zzz_after");
+		}
+
+		@ParameterizedTest
+		@ValueSource(classes = { LifecycleMethodArgumentInjectionWithConstructorInjectionTestCase.class,
+				LifecycleMethodArgumentInjectionWithFieldInjectionTestCase.class })
+		void supportsInjectingArgumentsIntoLifecycleMethods(Class<?> containerTemplateClass) {
+
+			var results = executeTestsForClass(containerTemplateClass);
+
+			results.allEvents().assertStatistics(stats -> stats.started(5).succeeded(5));
+		}
+
+		@ParameterizedTest
+		@ValueSource(classes = { CustomConverterAnnotationsWithLifecycleMethodsAndConstructorInjectionTestCase.class,
+				CustomConverterAnnotationsWithLifecycleMethodsAndFieldInjectionTestCase.class })
+		void convertersHaveAccessToTheirAnnotations(Class<?> containerTemplateClass) {
+
+			var results = executeTestsForClass(containerTemplateClass);
+
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
+		}
+
+		@ParameterizedTest
+		@ValueSource(classes = { ValidLifecycleMethodInjectionWithConstructorInjectionTestCase.class,
+				ValidLifecycleMethodInjectionWithFieldInjectionTestCase.class })
+		void supportsMixedInjectionsForLifecycleMethods(Class<?> containerTemplateClass) {
+
+			var results = executeTestsForClass(containerTemplateClass);
+
+			results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
+		}
+
+		@Test
+		void failsForLifecycleMethodWithInvalidParameters() {
+
+			var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class);
+
+			var expectedMessage = """
+					2 configuration errors:
+					- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: expected type 'int' but found 'long'
+					- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith
+					""";
+			expectedMessage = expectedMessage.trim() //
+					.replace("\n", System.lineSeparator()); // use platform-specific line separators
+
+			var failedResult = getFirstTestExecutionResult(results.containerEvents().failed());
+			assertThat(failedResult.getThrowable().orElseThrow()) //
+					.hasMessage(
+						"Invalid @BeforeArgumentSet lifecycle method declaration: static void %s.before(long,int)".formatted(
+							LifecycleMethodWithInvalidParametersTestCase.class.getName())) //
+					.cause().hasMessage(expectedMessage);
+		}
+
+		@Test
+		void failsForLifecycleMethodWithInvalidParameterOrder() {
+
+			var results = executeTestsForClass(LifecycleMethodWithInvalidParameterOrderTestCase.class);
+
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(message(
+						("@BeforeArgumentSet method [static void %s.before(%s,int,%s)] declares formal parameters in an invalid order: "
+								+ "argument aggregators must be declared after any indexed arguments and before any arguments resolved by another ParameterResolver.").formatted(
+									LifecycleMethodWithInvalidParameterOrderTestCase.class.getName(),
+									ArgumentsAccessor.class.getName(), ArgumentsAccessor.class.getName()))));
+		}
+
+		@Test
+		void failsForLifecycleMethodWithParameterAfterAggregator() {
+
+			var results = executeTestsForClass(LifecycleMethodWithParameterAfterAggregatorTestCase.class);
+
+			results.containerEvents().assertThatEvents() //
+					.haveExactly(1, finishedWithFailure(
+						message(it -> it.contains("No ParameterResolver registered for parameter [int value]"))));
+		}
 	}
 
 	// -------------------------------------------------------------------
@@ -484,6 +736,14 @@ private static Condition<UniqueId> lastSegmentType(@SuppressWarnings("SameParame
 			segmentType);
 	}
 
+	private static TestExecutionResult getFirstTestExecutionResult(Events events) {
+		return events.stream() //
+				.findFirst() //
+				.flatMap(Event::getPayload) //
+				.map(TestExecutionResult.class::cast) //
+				.orElseThrow();
+	}
+
 	@SuppressWarnings("JUnitMalformedDeclaration")
 	@ParameterizedClassWithNegativeAndPositiveValue
 	static class ConstructorInjectionTestCase {
@@ -1053,7 +1313,7 @@ void test() {
 
 	@ParameterizedClass
 	@ValueSource(ints = { 1, 2 })
-	static class NestedFieldInjectionTestCase {
+	static class NestedFieldInjectionTestCase extends LifecycleCallbacks {
 
 		@Parameter
 		int number;
@@ -1061,14 +1321,15 @@ static class NestedFieldInjectionTestCase {
 		@Nested
 		@ParameterizedClass
 		@ValueSource(strings = { "foo", "bar" })
-		class InnerTestCase {
+		class InnerTestCase extends LifecycleCallbacks {
 
 			@Parameter
 			String text;
 
 			@ParameterizedTest
 			@ValueSource(booleans = { true, false })
-			void test(boolean flag) {
+			void test(boolean flag, TestReporter reporter) {
+				reporter.publishEntry("test(" + number + ", " + text + ", " + flag + ")");
 				assertTrue(number > 0);
 				assertTrue(List.of("foo", "bar").contains(text));
 			}
@@ -1077,12 +1338,18 @@ void test(boolean flag) {
 
 	@ParameterizedClass
 	@ValueSource(ints = { 1, 2 })
-	record NestedConstructorInjectionTestCase(int number) {
+	static class NestedConstructorInjectionTestCase extends LifecycleCallbacks {
+
+		final int number;
+
+		NestedConstructorInjectionTestCase(int number) {
+			this.number = number;
+		}
 
 		@Nested
 		@ParameterizedClass
 		@ValueSource(strings = { "foo", "bar" })
-		class InnerTestCase {
+		class InnerTestCase extends LifecycleCallbacks {
 
 			final String text;
 
@@ -1092,13 +1359,49 @@ class InnerTestCase {
 
 			@ParameterizedTest
 			@ValueSource(booleans = { true, false })
-			void test(boolean flag) {
+			void test(boolean flag, TestReporter reporter) {
+				reporter.publishEntry("test(" + number + ", " + text + ", " + flag + ")");
 				assertTrue(number > 0);
 				assertTrue(List.of("foo", "bar").contains(text));
 			}
 		}
 	}
 
+	static class LifecycleCallbacks {
+
+		@BeforeAll
+		static void beforeAll(TestReporter reporter, TestInfo testInfo) {
+			reporter.publishEntry("beforeAll: " + testInfo.getTestClass().orElseThrow().getSimpleName());
+		}
+
+		@BeforeArgumentSet
+		static void beforeArgumentSet(TestReporter reporter, TestInfo testInfo) {
+			reporter.publishEntry("beforeArgumentSet: " + testInfo.getTestClass().orElseThrow().getSimpleName());
+		}
+
+		@BeforeEach
+		void beforeEach(TestReporter reporter, TestInfo testInfo) {
+			reporter.publishEntry(
+				"beforeEach: " + testInfo.getDisplayName() + " [" + this.getClass().getSimpleName() + "]");
+		}
+
+		@AfterEach
+		void afterEach(TestReporter reporter, TestInfo testInfo) {
+			reporter.publishEntry(
+				"afterEach: " + testInfo.getDisplayName() + " [" + this.getClass().getSimpleName() + "]");
+		}
+
+		@AfterArgumentSet
+		static void afterArgumentSet(TestReporter reporter, TestInfo testInfo) {
+			reporter.publishEntry("afterArgumentSet: " + testInfo.getTestClass().orElseThrow().getSimpleName());
+		}
+
+		@AfterAll
+		static void afterAll(TestReporter reporter, TestInfo testInfo) {
+			reporter.publishEntry("afterAll: " + testInfo.getTestClass().orElseThrow().getSimpleName());
+		}
+	}
+
 	@ParameterizedClass
 	@ValueSource(ints = { 1, 2 })
 	record ConstructorInjectionWithRegularNestedTestCase(int number) {
@@ -1169,7 +1472,7 @@ void test() {
 	}
 
 	@Retention(RetentionPolicy.RUNTIME)
-	@Target(ElementType.FIELD)
+	@Target({ ElementType.FIELD, ElementType.PARAMETER })
 	@Parameter
 	@AggregateWith(TimesTwoAggregator.class)
 	@interface TimesTwo {
@@ -1188,21 +1491,42 @@ List<String> methodSource() {
 
 		final List<String> fieldSource = List.of("bar");
 
+		@BeforeArgumentSet
+		void beforeArgumentSet1(TestReporter reporter) {
+			reporter.publishEntry("beforeArgumentSet1");
+		}
+
+		@BeforeArgumentSet
+		void beforeArgumentSet2(TestReporter reporter) {
+			reporter.publishEntry("beforeArgumentSet2");
+		}
+
+		@AfterArgumentSet
+		void afterArgumentSet1(TestReporter reporter) {
+			reporter.publishEntry("afterArgumentSet1");
+		}
+
+		@AfterArgumentSet
+		void afterArgumentSet2(TestReporter reporter) {
+			reporter.publishEntry("afterArgumentSet2");
+		}
+
 		@Parameter
 		private String value;
 
 		@Test
-		void test1(TestReporter reporter) {
-			publishReportEntry(reporter);
+		void test1(TestReporter reporter, TestInfo testInfo) {
+			publishReportEntry(reporter, testInfo);
 		}
 
 		@Test
-		void test2(TestReporter reporter) {
-			publishReportEntry(reporter);
+		void test2(TestReporter reporter, TestInfo testInfo) {
+			publishReportEntry(reporter, testInfo);
 		}
 
-		private void publishReportEntry(TestReporter reporter) {
+		private void publishReportEntry(TestReporter reporter, TestInfo testInfo) {
 			assertNotNull(value);
+			reporter.publishEntry(testInfo.getTestMethod().orElseThrow().getName());
 			reporter.publishEntry(Map.of( //
 				"instanceHashCode", Integer.toHexString(hashCode()), //
 				"value", value //
@@ -1381,4 +1705,368 @@ protected Object convert(Object source, Class<?> targetType) {
 			}
 		}
 	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	record NonStaticBeforeLifecycleMethodTestCase() {
+
+		@BeforeArgumentSet
+		void beforeArgumentSet() {
+			fail("should not be called");
+		}
+
+		@Test
+		void test() {
+			fail("should not be called");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	record NonStaticAfterLifecycleMethodTestCase() {
+
+		@AfterArgumentSet
+		void afterArgumentSet() {
+			fail("should not be called");
+		}
+
+		@Test
+		void test() {
+			fail("should not be called");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	record PrivateLifecycleMethodTestCase() {
+
+		@BeforeArgumentSet
+		private static void beforeArgumentSet() {
+			fail("should not be called");
+		}
+
+		@Test
+		void test() {
+			fail("should not be called");
+		}
+	}
+
+	static abstract class AbstractBaseLifecycleTestCase {
+
+		@BeforeArgumentSet
+		static void zzz_before(TestReporter reporter) {
+			reporter.publishEntry("zzz_before");
+		}
+
+		@AfterArgumentSet
+		static void zzz_after(TestReporter reporter) {
+			reporter.publishEntry("zzz_after");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	static class LifecycleMethodsFromSuperclassTestCase extends AbstractBaseLifecycleTestCase {
+
+		@BeforeArgumentSet
+		static void aaa_before(TestReporter reporter) {
+			reporter.publishEntry("aaa_before");
+		}
+
+		@AfterArgumentSet
+		static void aaa_after(TestReporter reporter) {
+			reporter.publishEntry("aaa_after");
+		}
+
+		@Test
+		void test(TestReporter reporter) {
+			reporter.publishEntry("test");
+		}
+	}
+
+	static abstract class AbstractBaseLifecycleWithErrorsTestCase {
+
+		@BeforeArgumentSet
+		static void zzz_before(TestReporter reporter) {
+			reporter.publishEntry("zzz_before");
+			fail("zzz_before");
+		}
+
+		@AfterArgumentSet
+		static void zzz_after(TestReporter reporter) {
+			reporter.publishEntry("zzz_after");
+			fail("zzz_after");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	static class LifecycleMethodsErrorHandlingTestCase extends AbstractBaseLifecycleWithErrorsTestCase {
+
+		@BeforeArgumentSet
+		static void aaa_before(TestReporter reporter) {
+			fail("should not be called");
+		}
+
+		@AfterArgumentSet
+		static void aaa_after(TestReporter reporter) {
+			reporter.publishEntry("aaa_after");
+			fail("aaa_after");
+		}
+
+		@Test
+		void test(TestReporter reporter) {
+			reporter.publishEntry("test");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	record LifecycleMethodArgumentInjectionWithConstructorInjectionTestCase(
+			@ConvertWith(AtomicIntegerConverter.class) AtomicInteger counter) {
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(AtomicInteger counter) {
+			assertEquals(2, counter.incrementAndGet());
+		}
+
+		@AfterArgumentSet(injectArguments = true)
+		static void after(AtomicInteger counter) {
+			assertEquals(4, counter.get());
+		}
+
+		@Test
+		void test1() {
+			this.counter.incrementAndGet();
+		}
+
+		@Test
+		void test2() {
+			this.counter.incrementAndGet();
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	static class LifecycleMethodArgumentInjectionWithFieldInjectionTestCase {
+
+		@Parameter
+		@ConvertWith(AtomicIntegerConverter.class)
+		AtomicInteger counter;
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(AtomicInteger counter) {
+			assertEquals(2, counter.incrementAndGet());
+		}
+
+		@AfterArgumentSet(injectArguments = true)
+		static void after(AtomicInteger counter) {
+			assertEquals(4, counter.get());
+		}
+
+		@Test
+		void test1() {
+			this.counter.incrementAndGet();
+		}
+
+		@Test
+		void test2() {
+			this.counter.incrementAndGet();
+		}
+	}
+
+	static class AtomicIntegerConverter extends SimpleArgumentConverter {
+		@Override
+		protected Object convert(Object source, Class<?> targetType) {
+			return new AtomicInteger((Integer) source);
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(strings = "foo")
+	record CustomConverterAnnotationsWithLifecycleMethodsAndConstructorInjectionTestCase(
+			@CustomConversion String value) {
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(String value) {
+			assertEquals("foo", value);
+		}
+
+		@Test
+		void test() {
+			assertEquals("foo", this.value);
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(strings = "foo")
+	static class CustomConverterAnnotationsWithLifecycleMethodsAndFieldInjectionTestCase {
+
+		@Parameter
+		@CustomConversion
+		String value;
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(String value) {
+			assertEquals("foo", value);
+		}
+
+		@Test
+		void test() {
+			assertEquals("foo", this.value);
+		}
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@Target({ ElementType.PARAMETER, ElementType.FIELD })
+	@ConvertWith(CustomConversion.Converter.class)
+	@interface CustomConversion {
+
+		class Converter implements ArgumentConverter {
+			@Override
+			public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
+				assertNotNull(context.getParameter().getAnnotation(CustomConversion.class));
+				return source;
+			}
+
+			@Override
+			public Object convert(Object source, FieldContext context) throws ArgumentConversionException {
+				assertNotNull(context.getField().getAnnotation(CustomConversion.class));
+				return source;
+			}
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	static class ValidLifecycleMethodInjectionWithConstructorInjectionTestCase
+			extends AbstractValidLifecycleMethodInjectionTestCase {
+
+		private final AtomicInteger value;
+
+		ValidLifecycleMethodInjectionWithConstructorInjectionTestCase(
+				@ConvertWith(AtomicIntegerConverter.class) AtomicInteger value) {
+			this.value = value;
+		}
+
+		@Test
+		void test() {
+			assertEquals(5, this.value.getAndIncrement());
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	static class ValidLifecycleMethodInjectionWithFieldInjectionTestCase
+			extends AbstractValidLifecycleMethodInjectionTestCase {
+
+		@Parameter
+		@ConvertWith(AtomicIntegerConverter.class)
+		AtomicInteger value;
+
+		@Test
+		void test() {
+			assertEquals(5, this.value.getAndIncrement());
+		}
+	}
+
+	abstract static class AbstractValidLifecycleMethodInjectionTestCase {
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before0() {
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before1(AtomicInteger value) {
+			value.incrementAndGet();
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before2(ArgumentsAccessor accessor) {
+			assertEquals(1, accessor.getInteger(0));
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before3(AtomicInteger value, TestInfo testInfo) {
+			assertEquals("[1] value=1", testInfo.getDisplayName());
+			value.incrementAndGet();
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before4(ArgumentsAccessor accessor, TestInfo testInfo) {
+			assertEquals(1, accessor.getInteger(0));
+			assertEquals("[1] value=1", testInfo.getDisplayName());
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before4(AtomicInteger value, ArgumentsAccessor accessor) {
+			assertEquals(1, accessor.getInteger(0));
+			value.incrementAndGet();
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before5(AtomicInteger value, ArgumentsAccessor accessor, TestInfo testInfo) {
+			assertEquals(1, accessor.getInteger(0));
+			assertEquals("[1] value=1", testInfo.getDisplayName());
+			value.incrementAndGet();
+		}
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before6(@TimesTwo int valueTimesTwo) {
+			assertEquals(2, valueTimesTwo);
+		}
+
+		@AfterArgumentSet(injectArguments = true)
+		static void after(AtomicInteger value, ArgumentsAccessor accessor, TestInfo testInfo) {
+			assertEquals(6, value.get());
+			assertEquals(1, accessor.getInteger(0));
+			assertEquals("[1] value=1", testInfo.getDisplayName());
+		}
+	}
+
+	@ParameterizedClass
+	@CsvSource("1, 2")
+	record LifecycleMethodWithInvalidParametersTestCase(int value, int anotherValue) {
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(long value, @ConvertWith(CustomIntegerToStringConverter.class) int anotherValue) {
+			fail("should not be called");
+		}
+
+		@Test
+		void test() {
+			fail("should not be called");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	record LifecycleMethodWithInvalidParameterOrderTestCase(int value) {
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(ArgumentsAccessor accessor1, int value, ArgumentsAccessor accessor2) {
+			fail("should not be called");
+		}
+
+		@Test
+		void test() {
+			fail("should not be called");
+		}
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	record LifecycleMethodWithParameterAfterAggregatorTestCase(int value) {
+
+		@BeforeArgumentSet(injectArguments = true)
+		static void before(@TimesTwo int valueTimesTwo, int value) {
+			fail("should not be called");
+		}
+
+		@Test
+		void test() {
+			fail("should not be called");
+		}
+	}
+
 }