diff --git a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java
index 443780a40..5e2373a67 100644
--- a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java
+++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java
@@ -24,6 +24,7 @@
import io.qameta.allure.model.Status;
import io.qameta.allure.util.ParameterUtils;
import io.qameta.allure.util.ResultsUtils;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
@@ -31,7 +32,9 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.IdentityHashMap;
import java.util.List;
+import java.util.Set;
/**
* Reports JUnit Jupiter fixture and parameter execution details to Allure.
@@ -42,17 +45,23 @@
* Platform unique ids — see {@link AllureJunitPlatform#scopeKey(String)} and
* {@link AllureJunitPlatform#testKey(String)}.
*/
-public class AllureJupiter implements InvocationInterceptor {
+public class AllureJupiter implements InvocationInterceptor, BeforeEachCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(AllureJupiter.class);
private static final String TEST = "test";
private static final String TEMPLATE = "template";
+ private static final String PARAMETERS = "parameters";
private static final String PREPARE = "prepare";
private static final String TEAR_DOWN = "tear_down";
+ private static final String CAPTURED_PARAMETERS = "captured_parameters";
- // parameterized class support requires the ParameterInfo API of junit-jupiter-params 6.x
- private static final boolean CLASS_PARAMETERS_SUPPORTED = isClassAvailableOnClasspath("org.junit.jupiter.params.ParameterInfo");
+ private static final boolean CURRENT_PARAMETER_INFO_SUPPORTED = isClassAvailableOnClasspath(
+ "org.junit.jupiter.params.ParameterInfo"
+ );
+ private static final boolean LEGACY_PARAMETER_INFO_SUPPORTED = isClassAvailableOnClasspath(
+ "org.junit.jupiter.params.support.ParameterInfo"
+ );
/**
* Returns the lifecycle. Resolved at call time, so the extension follows process-wide lifecycle swaps.
@@ -63,6 +72,24 @@ protected AllureLifecycle getLifecycle() {
return Allure.getLifecycle();
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void beforeEach(final ExtensionContext extensionContext) {
+ final Method testMethod = extensionContext.getRequiredTestMethod();
+ if (!shouldHandle(extensionContext, PARAMETERS, testMethod)) {
+ return;
+ }
+ final List parameters = getParameters(extensionContext);
+ if (parameters.isEmpty()) {
+ return;
+ }
+ extensionContext.getStore(NAMESPACE)
+ .put(CAPTURED_PARAMETERS, new ParameterCapture(parameters));
+ addParameters(extensionContext, parameters);
+ }
+
/**
* {@inheritDoc}
*/
@@ -75,7 +102,7 @@ public void interceptTestMethod(final Invocation invocation,
invocation.proceed();
return;
}
- addParameters(extensionContext, getClassParameters(invocationContext, extensionContext));
+ replaceCapturedParameters(extensionContext, getClassParameters(invocationContext, extensionContext));
invocation.proceed();
}
@@ -93,7 +120,7 @@ public void interceptTestTemplateMethod(final Invocation invocation,
}
final List testParameters = new ArrayList<>(getClassParameters(invocationContext, extensionContext));
testParameters.addAll(getArgumentParameters(invocationContext));
- addParameters(extensionContext, testParameters);
+ replaceCapturedParameters(extensionContext, testParameters);
invocation.proceed();
}
@@ -108,15 +135,53 @@ private void addParameters(final ExtensionContext extensionContext,
);
}
+ private void replaceCapturedParameters(final ExtensionContext extensionContext,
+ final List testParameters) {
+ final ParameterCapture capture = extensionContext.getStore(NAMESPACE)
+ .remove(CAPTURED_PARAMETERS, ParameterCapture.class);
+ if (capture == null) {
+ addParameters(extensionContext, testParameters);
+ return;
+ }
+
+ // ParameterInfo exposes source arguments before setup. Once the invocation exists, prefer its converted
+ // values and remove only the objects captured here so parameters added by user code remain untouched.
+ final Set capturedParameters = Collections.newSetFromMap(new IdentityHashMap<>());
+ capturedParameters.addAll(capture.parameters());
+ getLifecycle().updateTest(
+ AllureJunitPlatform.testKey(extensionContext.getUniqueId()),
+ testResult -> {
+ testResult.getParameters().removeIf(capturedParameters::contains);
+ testResult.getParameters().addAll(testParameters);
+ }
+ );
+ }
+
+ private List getParameters(final ExtensionContext extensionContext) {
+ if (CURRENT_PARAMETER_INFO_SUPPORTED) {
+ return AllureJupiterParameterInfoSupport.getParameters(extensionContext);
+ }
+ if (LEGACY_PARAMETER_INFO_SUPPORTED) {
+ return AllureJupiterLegacyParameterInfoSupport.getParameters(extensionContext);
+ }
+ return Collections.emptyList();
+ }
+
private List getClassParameters(final ReflectiveInvocationContext invocationContext,
final ExtensionContext extensionContext) {
- if (!CLASS_PARAMETERS_SUPPORTED) {
- return Collections.emptyList();
+ if (CURRENT_PARAMETER_INFO_SUPPORTED) {
+ return AllureJupiterParameterInfoSupport.getClassParameters(
+ extensionContext,
+ invocationContext.getExecutable()
+ );
}
- return AllureJupiterParameterInfoSupport.getClassParameters(
- extensionContext,
- invocationContext.getExecutable()
- );
+ if (LEGACY_PARAMETER_INFO_SUPPORTED) {
+ return AllureJupiterLegacyParameterInfoSupport.getClassParameters(
+ extensionContext,
+ invocationContext.getExecutable()
+ );
+ }
+ return Collections.emptyList();
}
private List getArgumentParameters(final ReflectiveInvocationContext invocationContext) {
@@ -258,4 +323,7 @@ private boolean shouldHandle(final ExtensionContext extensionContext,
.getOrComputeIfAbsent(key, ignored -> marker);
return marker.equals(storedMarker);
}
+
+ private record ParameterCapture(List parameters) {
+ }
}
diff --git a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java
new file mode 100644
index 000000000..261ecef53
--- /dev/null
+++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016-2026 Qameta Software Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.qameta.allure.jupiter;
+
+import io.qameta.allure.model.Parameter;
+import io.qameta.allure.util.ParameterUtils;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+import org.junit.jupiter.params.support.ParameterDeclaration;
+import org.junit.jupiter.params.support.ParameterInfo;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Reads the legacy JUnit Jupiter {@link ParameterInfo} API to report arguments with junit-jupiter-params 5.13. The
+ * API is deprecated in JUnit 6 — every reference to it stays inside this class, which is loaded only after a
+ * classpath check and only when the replacement API is unavailable.
+ */
+@SuppressWarnings({"deprecation", "removal"})
+/* package-private */ final class AllureJupiterLegacyParameterInfoSupport {
+
+ private AllureJupiterLegacyParameterInfoSupport() {
+ throw new IllegalStateException("do not instance");
+ }
+
+ /**
+ * Collects all arguments available for the current parameterized invocation as Allure parameters.
+ *
+ * @param extensionContext the extension context of the running test
+ * @return the Allure parameters, empty when the test is not parameterized
+ */
+ static List getParameters(final ExtensionContext extensionContext) {
+ return collectParameters(extensionContext, info -> true);
+ }
+
+ /**
+ * Collects the arguments of every enclosing parameterized class invocation as Allure parameters, outermost
+ * first. The test method's own arguments are excluded: they are reported from the invocation context.
+ *
+ * @param extensionContext the extension context of the running test
+ * @param testMethod the running test method
+ * @return the class-level Allure parameters, empty when the test runs outside parameterized classes
+ */
+ static List getClassParameters(final ExtensionContext extensionContext,
+ final Method testMethod) {
+ return collectParameters(
+ extensionContext,
+ info -> !testMethod.equals(info.getDeclarations().getSourceElement())
+ );
+ }
+
+ private static List collectParameters(final ExtensionContext extensionContext,
+ final Predicate filter) {
+ final List result = new ArrayList<>();
+ final Set visited = Collections.newSetFromMap(new IdentityHashMap<>());
+ Optional current = Optional.of(extensionContext);
+ while (current.isPresent()) {
+ final ParameterInfo info = ParameterInfo.get(current.get());
+ if (Objects.isNull(info)) {
+ break;
+ }
+ if (visited.add(info) && filter.test(info)) {
+ // contexts are walked inner to outer: prepend so outer class arguments come first
+ result.addAll(0, toParameters(info));
+ }
+ current = current.get().getParent();
+ }
+ return result;
+ }
+
+ private static List toParameters(final ParameterInfo info) {
+ final ArgumentsAccessor arguments = info.getArguments();
+ final List result = new ArrayList<>();
+ for (final ParameterDeclaration declaration : info.getDeclarations().getAll()) {
+ final int index = declaration.getParameterIndex();
+ if (index < 0 || index >= arguments.size()) {
+ continue;
+ }
+ result.add(
+ ParameterUtils.createParameter(
+ declaration.getAnnotatedElement(),
+ arguments.get(index),
+ declaration.getParameterName().orElse("arg" + index)
+ )
+ );
+ }
+ return result;
+ }
+}
diff --git a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java
index 7e8ca5b1c..a7507dd56 100644
--- a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java
+++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java
@@ -30,11 +30,12 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Predicate;
/**
- * Reads the JUnit Jupiter {@link ParameterInfo} API to report parameterized class arguments. The API exists since
- * junit-jupiter-params 6.0 — every reference to it stays inside this class, which is loaded only after a classpath
- * check.
+ * Reads the JUnit Jupiter {@link ParameterInfo} API to report parameterized invocation arguments. The API exists
+ * since junit-jupiter-params 6.0 — every reference to it stays inside this class, which is loaded only after a
+ * classpath check.
*/
/* package-private */ final class AllureJupiterParameterInfoSupport {
@@ -42,6 +43,16 @@ private AllureJupiterParameterInfoSupport() {
throw new IllegalStateException("do not instance");
}
+ /**
+ * Collects all arguments available for the current parameterized invocation as Allure parameters.
+ *
+ * @param extensionContext the extension context of the running test
+ * @return the Allure parameters, empty when the test is not parameterized
+ */
+ static List getParameters(final ExtensionContext extensionContext) {
+ return collectParameters(extensionContext, info -> true);
+ }
+
/**
* Collects the arguments of every enclosing parameterized class invocation as Allure parameters, outermost
* first. The test method's own arguments are excluded: they are reported from the invocation context.
@@ -52,6 +63,14 @@ private AllureJupiterParameterInfoSupport() {
*/
static List getClassParameters(final ExtensionContext extensionContext,
final Method testMethod) {
+ return collectParameters(
+ extensionContext,
+ info -> !testMethod.equals(info.getDeclarations().getSourceElement())
+ );
+ }
+
+ private static List collectParameters(final ExtensionContext extensionContext,
+ final Predicate filter) {
final List result = new ArrayList<>();
final Set visited = Collections.newSetFromMap(new IdentityHashMap<>());
Optional current = Optional.of(extensionContext);
@@ -60,7 +79,7 @@ static List getClassParameters(final ExtensionContext extensionContex
if (Objects.isNull(info)) {
break;
}
- if (visited.add(info) && !testMethod.equals(info.getDeclarations().getSourceElement())) {
+ if (visited.add(info) && filter.test(info)) {
// contexts are walked inner to outer: prepend so outer class arguments come first
result.addAll(0, toParameters(info));
}
diff --git a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java
index 3fb1916de..60e9d627f 100644
--- a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java
+++ b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java
@@ -22,6 +22,7 @@
import io.qameta.allure.jupiter.features.GlobalErrorTests;
import io.qameta.allure.jupiter.features.NestedTemplatesTests;
import io.qameta.allure.jupiter.features.ParamAnnotationTests;
+import io.qameta.allure.jupiter.features.ParameterizedBeforeEachTests;
import io.qameta.allure.jupiter.features.ParameterizedClassTests;
import io.qameta.allure.model.FixtureResult;
import io.qameta.allure.model.GlobalError;
@@ -135,6 +136,45 @@ void shouldReportFailedBeforeEachFixture() {
.containsExactly(testResult.getUuid());
}
+ @Test
+ @AllureFeatures.Fixtures
+ @AllureFeatures.Parameters
+ @AllureFeatures.History
+ void shouldKeepParametersWhenBeforeEachFails() {
+ final AllureResults failedResults;
+ ParameterizedBeforeEachTests.failBeforeEach(true);
+ try {
+ failedResults = runClasses(ParameterizedBeforeEachTests.class);
+ } finally {
+ ParameterizedBeforeEachTests.failBeforeEach(false);
+ }
+ final AllureResults passedResults = runClasses(ParameterizedBeforeEachTests.class);
+
+ assertThat(failedResults.getTestResults()).singleElement()
+ .extracting(TestResult::getStatus)
+ .isEqualTo(Status.BROKEN);
+ assertThat(passedResults.getTestResults()).singleElement()
+ .extracting(TestResult::getStatus)
+ .isEqualTo(Status.PASSED);
+
+ final TestResult failed = failedResults.getTestResults().get(0);
+ final TestResult passed = passedResults.getTestResults().get(0);
+ assertThat(failed.getParameters())
+ .extracting(Parameter::getName)
+ .containsExactlyInAnyOrder("UniqueId", "first", "second");
+ assertThat(failed.getParameters())
+ .filteredOn(parameter -> !"UniqueId".equals(parameter.getName()))
+ .extracting(Parameter::getName, Parameter::getValue)
+ .containsExactlyInAnyOrder(
+ tuple("first", "first value"),
+ tuple("second", "second value")
+ );
+ assertThat(failed.getParameters())
+ .containsExactlyInAnyOrderElementsOf(passed.getParameters());
+ assertThat(failed.getTestCaseId()).isEqualTo(passed.getTestCaseId());
+ assertThat(failed.getHistoryId()).isEqualTo(passed.getHistoryId());
+ }
+
@Test
void shouldReportGlobalErrorFromJupiterTest() {
final AllureResults results = runClasses(GlobalErrorTests.class);
diff --git a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java
new file mode 100644
index 000000000..ce165deb6
--- /dev/null
+++ b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016-2026 Qameta Software Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.qameta.allure.jupiter.features;
+
+import io.qameta.allure.Param;
+import io.qameta.allure.jupiter.AllureJupiter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+@ExtendWith(AllureJupiter.class)
+public class ParameterizedBeforeEachTests {
+
+ private static boolean failBeforeEach;
+
+ public static void failBeforeEach(final boolean fail) {
+ failBeforeEach = fail;
+ }
+
+ @BeforeEach
+ void setUp() {
+ if (failBeforeEach) {
+ throw new IllegalStateException("fail in beforeEach");
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource("first value, second value")
+ void parameterizedTest(@Param("first") final String first,
+ @Param("second") final String second) {
+ }
+}