`JUnit4to5Migration` moves `Parameterized` parameter assignment after `@BeforeEach`, so setup methods read null
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 70/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- java
- Domain
- testing-qa, tooling
Research direction
Start with org.openrewrite.java.testing.junit5.ParameterizedRunnerToParameterized and its tests in ParameterizedRunnerToParameterizedTest, then inspect JUnit4to5Migration and junit5.yml for composite ordering. Reproduce both constructor- and field-injection cases through the composite recipe; done means setup methods no longer read unassigned parameters and the migration tests pass.
Written by the indexing model from the issue text.
Description
What happens
Under JUnit 4, @RunWith(Parameterized.class) supplies parameters before any @Before method
runs — through the constructor for constructor injection, or through @Parameter field assignment
for field injection. A @Before method may therefore rely on parameter-derived state.
JUnit4to5Migration replaces that with an init<ClassName>(...) method invoked as the first
statement of the @ParameterizedTest method. JUnit 5 runs @BeforeEach before the test method, so
the setup method now executes before the parameters are assigned and reads uninitialised fields.
The migrated code compiles. The failure appears only when the tests run, as a
NullPointerException inside the setup method.
Minimal reproduction 1 — constructor injection
Before, passing (Tests run: 2, Failures: 0):
package com.example;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class ParamSetupTest {
private final List<String> _names;
private List<String> _upper;
public ParamSetupTest(String... names) {
_names = new ArrayList<>(Arrays.asList(names));
}
@Parameters
public static Collection<Object[]> testCases() {
return Arrays.asList(new Object[][] {
{new String[] {"a", "b"}},
{new String[] {"c"}},
});
}
@Before
public void before() {
_upper = _names.stream().map(String::toUpperCase).toList();
}
@Test
public void namesAreUppercased() {
assertEquals(_names.size(), _upper.size());
}
}
After org.openrewrite.java.testing.junit5.JUnit4to5Migration:
package com.example;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ParamSetupTest {
private List<String> _names;
private List<String> _upper;
public void initParamSetupTest(String... names) {
_names = new ArrayList<>(Arrays.asList(names));
}
public static Collection<Object[]> testCases() {
return Arrays.asList(new Object[][] {
{new String[] {"a", "b"}},
{new String[] {"c"}},
});
}
@BeforeEach
public void before() {
_upper = _names.stream().map(String::toUpperCase).toList();
}
@MethodSource("testCases")
@ParameterizedTest
public void namesAreUppercased(String... names) {
initParamSetupTest(names);
assertEquals(_names.size(), _upper.size());
}
}
java.lang.NullPointerException: Cannot invoke "java.util.List.stream()" because "this._names" is null
at com.example.ParamSetupTest.before(ParamSetupTest.java:32)
Tests run: 2, Failures: 0, Errors: 2
before() runs as @BeforeEach, ahead of the test method that would have called
initParamSetupTest(...). Note also that final is removed from _names so the renamed
constructor can assign it, losing the guarantee that the field is written exactly once.
Minimal reproduction 2 — field injection
The same failure occurs with @Parameter fields:
@RunWith(Parameterized.class)
public class FieldParamTest {
@Parameter(0)
public String name;
private String upper;
@Parameters
public static Collection<Object[]> testCases() {
return Arrays.asList(new Object[][] {{"a"}, {"b"}});
}
@Before
public void before() {
upper = name.toUpperCase();
}
@Test
public void nameIsUppercased() {
assertEquals(name.toUpperCase(), upper);
}
}
becomes
public class FieldParamTest {
public String name;
private String upper;
public static Collection<Object[]> testCases() {
return Arrays.asList(new Object[][] {{"a"}, {"b"}});
}
@BeforeEach
public void before() {
upper = name.toUpperCase();
}
@MethodSource("testCases")
@ParameterizedTest
public void nameIsUppercased(String name) {
initFieldParamTest(name);
assertEquals(name.toUpperCase(), upper);
}
public void initFieldParamTest(String name) {
this.name = name;
}
}
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "this.name" is null
at com.example.FieldParamTest.before(FieldParamTest.java:23)
Tests run: 2, Failures: 0, Errors: 2
Why this matters
This is a silent behavioural regression, not a build break — the module compiles and the failure
only shows up when the tests run, as a NullPointerException inside what used to be reliable setup
code. It can affect any @Before that reads parameter-derived state, which is a common pattern for
shared fixtures in parameterized tests, and it hits both injection styles for different underlying
reasons:
| Path | Setup call added? | Outcome |
|---|---|---|
| Constructor injection | No — never generated on this path | NullPointerException at runtime |
Field injection, run via the full JUnit4to5Migration composite |
No — @Before is already @BeforeEach by the time the scanner looks for it |
NullPointerException at runtime |
| Field injection, recipe run in isolation | Yes — this.before() is correctly added |
passes |
The last row matters: the recipe has the right idea and gets it right when run alone. Both real-world
reproductions fail anyway, for two independent reasons.
Why the existing tests don't cover it
ParameterizedRunnerToParameterized already knows the setup method has to be re-invoked. Its scanner
records the @Before method name:
private static final AnnotationMatcher BEFORE = new AnnotationMatcher("@org.junit.Before");
...
if (BEFORE.matches(annotation)) {
params.put(BEFORE_METHOD_NAME, method.getSimpleName());
}
and buildInitMethodDeclarationTemplate appends a call to it:
if (beforeMethodName != null) {
initMethodTemplate.append(" this.").append(beforeMethodName).append("();\n");
}
Two separate things stop that from reaching the output.
The template is only used for field injection. buildInitMethodDeclarationTemplate is applied
under if (!isConstructorInjection). For constructor injection the recipe instead renames the
existing constructor:
// Change constructor to test init method
if (isConstructorInjection && m.isConstructor()) {
m = m.withName(m.getName().withSimpleName(initMethodName));
The renamed constructor keeps its original body, so no call to the setup method is ever added.
Running the recipe on its own against reproduction 1, with @Before still present, confirms this —
the generated initParamSetupTest contains only the original assignment.
On the field-injection path the matcher no longer matches once run inside the composite. Run in
isolation, the recipe produces the intended output:
public void initFieldParamTest(String name) {
this.name = name;
this.before();
}
Run as part of JUnit4to5Migration the this.before(); line is absent, because
UpdateBeforeAfterAnnotations is listed earlier in the composite recipe (junit5.yml:
UpdateBeforeAfterAnnotations before ParameterizedRunnerToParameterized in JUnit4to5Migration's
recipeList) and has already rewritten @Before to @BeforeEach by the time this scanner runs.
BEFORE matches @org.junit.Before only, so beforeMethodName stays null.
Even with the call restored, the setup method would still be annotated @BeforeEach and run a
second time before the parameters exist, so the annotation itself needs handling rather than only
the call.
ParameterizedRunnerToParameterizedTest exercises the recipe directly rather than through
JUnit4to5Migration, so @Before is always still @org.junit.Before when the scanner runs and the
matcher always fires — the composite ordering that defeats it is never reproduced. The suite also
has no constructor-injection test with a @Before method at all, so the missing this.before() call
on that path has nothing in the expected output to show up against.
Environment
- rewrite-testing-frameworks
3.44.0(tagv3.44.0); - rewrite-maven-plugin
6.46.1 - Recipe run:
org.openrewrite.java.testing.junit5.JUnit4to5Migration, which reaches
org.openrewrite.java.testing.junit5.ParameterizedRunnerToParameterized
- Dominant language
- Java
- Stars
- 100
- Forks
- 109
- Avg merge
- 3h 43m
- Merged PRs (30d)
- 82
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openrewrite/rewrite-testing-frameworks
-
Recipe quality triage: remaining fixes, wiring and polish (A1–O8)May be free again @tashen247 claimed this 17 days ago, and no pull request is open. Opengood first issue
Difficulty 4/5 3-5 days Newbie friendliness 48/100
openrewrite/rewrite-testing-frameworks#1211 · 2 comments ·
Maintainers usually reply within 1 day
-
JUnit 4 to 5 Migration: Tests with Multiple @Before Methods Fail Due to Arbitrary Execution OrderPossibly taken @ApekshaAmbala claimed this 19 days ago. Openjunit recipe
Difficulty 4/5 3-5 days Newbie friendliness 55/100
openrewrite/rewrite-testing-frameworks#1207 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
JUnit4to5Migration - undiscovered or invalid Kotlin testsPossibly taken @hugoncosta claimed this 28 days ago. Openbug
Difficulty 4/5 3-5 days Newbie friendliness 42/100
openrewrite/rewrite-testing-frameworks#1188 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
openrewrite/rewrite-testing-frameworks#1167 ·
Maintainers usually reply within 1 day
-
`ParameterizedRunnerToParameterized` silently skips classes whose `@Parameters` method is inherited, leaving a partially migrated class thatPossibly taken @mohitduhan19 claimed this 43 days ago. Openbug
Difficulty 4/5 3-5 days Newbie friendliness 68/100
openrewrite/rewrite-testing-frameworks#1126 ·
Maintainers usually reply within 1 day
All issues in openrewrite/rewrite-testing-frameworks
Similar issues
-
Four TestConstraintBasedAlgorithm tests pass when nothing is thrownPossibly taken @sharanggupta claimed this today. Open
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
-
bug documentation good first issue help wanted priority low size XS
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
martin-francois/symphony-trello#1351 ·
Maintainers usually reply within 1 day
-
Add test coverage for ConvertUtil.parseDate() with NULL inputPossibly taken @mirajul-mollah7 claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 78/100
eclipse-sw360/sw360#4792 · 1 comment ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
aws/aws-emulator#30 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 4 days