What happens
When a JUnit 3 test class has a TestCase(String name) constructor whose body contains anything
besides super(name), MigrateJUnitTestCase removes the super(name) call and keeps the
constructor. The class compiles, so the migration looks successful, but JUnit 5 can no longer
instantiate it and every test in the class errors at run time.
Minimal reproduction
Before:
import junit.framework.TestCase;
public class MathTest extends TestCase {
private boolean initialized;
public MathTest(String testName) {
super(testName);
initialized = true;
}
public void testAdd() {
assertTrue(initialized);
}
}
After org.openrewrite.java.testing.junit5.MigrateJUnitTestCase:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MathTest {
private boolean initialized;
public MathTest(String testName) {
initialized = true;
}
@Test
public void testAdd() {
assertTrue(initialized);
}
}
Everything else is migrated correctly. The class compiles. Running it gives:
org.junit.jupiter.api.extension.ParameterResolutionException:
No ParameterResolver registered for parameter [java.lang.String arg0]
in constructor [public MathTest(java.lang.String)]
Why this matters
JUnit 5 constructs a fresh instance of the test class per test method and resolves constructor
parameters through registered ParameterResolver extensions. There is no built-in resolver for a
bare String, so instantiation fails before any test method runs and the whole class errors out.
The String parameter is not arbitrary user API. TestCase(String name) was JUnit 3's own
mechanism: the runner passed the test method name in and getName() returned it. JUnit 5 dropped
that model entirely, so once extends TestCase is gone the parameter is a leftover of a contract
that no longer exists and nothing will ever supply it.
Two properties make this worse than a compile error:
- It is silent. The build is green. The failure only appears when the tests are executed, and in
a large migration that can be a long way from the change.
- Real setup is stranded.
initialized = true above — and in practice things like a
BaseInit.init() call — used to run once per test instance. It is now in a constructor that is
never invoked. Simply deleting the parameter to make the class instantiable would drop that
initialisation, so the statements need to move to @BeforeEach.
Whether the migration output fails at compile time or at run time depends on an incidental detail of
the original constructor body:
| Original constructor body |
After super removal |
Outcome |
super(name); |
empty, constructor deleted |
callers no longer compile (separate issue) |
super(name); doSomething(); |
non-empty, constructor kept |
compiles, class cannot be instantiated |
Why the existing tests don't cover it
MigrateJUnitTestCaseTest.constructorWithAdditionalStatementsIsKept asserts exactly this
transformation:
public class AppTest extends TestCase {
private final String name;
public AppTest(String testName) {
super(testName);
this.name = testName;
}
}
becomes
public class AppTest {
private final String name;
public AppTest(String testName) {
this.name = testName;
}
}
Keeping user-written statements is clearly the right instinct. The gap is that the fixture has no
@Test methods, so the resulting class is never executed and the ParameterResolutionException is
not observed. Adding a test method to that same fixture is enough to expose it.
When the enclosing class was a TestCase and the retained constructor's only parameter is the
JUnit 3 test name, the migration could move the remaining statements into a @BeforeEach method and
drop the constructor, preserving both the statements and their per-test execution semantics:
@BeforeEach
public void setUp() {
initialized = true;
}
If that is considered too invasive, leaving the constructor but reporting it would at least stop the
result looking like a clean migration.
Environment
- rewrite-testing-frameworks
3.44.0 (tag v3.44.0); also reproduces on main
- Recipe:
org.openrewrite.java.testing.junit5.MigrateJUnitTestCase
What happens
When a JUnit 3 test class has a
TestCase(String name)constructor whose body contains anythingbesides
super(name),MigrateJUnitTestCaseremoves thesuper(name)call and keeps theconstructor. The class compiles, so the migration looks successful, but JUnit 5 can no longer
instantiate it and every test in the class errors at run time.
Minimal reproduction
Before:
After
org.openrewrite.java.testing.junit5.MigrateJUnitTestCase:Everything else is migrated correctly. The class compiles. Running it gives:
Why this matters
JUnit 5 constructs a fresh instance of the test class per test method and resolves constructor
parameters through registered
ParameterResolverextensions. There is no built-in resolver for abare
String, so instantiation fails before any test method runs and the whole class errors out.The
Stringparameter is not arbitrary user API.TestCase(String name)was JUnit 3's ownmechanism: the runner passed the test method name in and
getName()returned it. JUnit 5 droppedthat model entirely, so once
extends TestCaseis gone the parameter is a leftover of a contractthat no longer exists and nothing will ever supply it.
Two properties make this worse than a compile error:
a large migration that can be a long way from the change.
initialized = trueabove — and in practice things like aBaseInit.init()call — used to run once per test instance. It is now in a constructor that isnever invoked. Simply deleting the parameter to make the class instantiable would drop that
initialisation, so the statements need to move to
@BeforeEach.Whether the migration output fails at compile time or at run time depends on an incidental detail of
the original constructor body:
superremovalsuper(name);super(name); doSomething();Why the existing tests don't cover it
MigrateJUnitTestCaseTest.constructorWithAdditionalStatementsIsKeptasserts exactly thistransformation:
becomes
Keeping user-written statements is clearly the right instinct. The gap is that the fixture has no
@Testmethods, so the resulting class is never executed and theParameterResolutionExceptionisnot observed. Adding a test method to that same fixture is enough to expose it.
When the enclosing class was a
TestCaseand the retained constructor's only parameter is theJUnit 3 test name, the migration could move the remaining statements into a
@BeforeEachmethod anddrop the constructor, preserving both the statements and their per-test execution semantics:
If that is considered too invasive, leaving the constructor but reporting it would at least stop the
result looking like a clean migration.
Environment
3.44.0(tagv3.44.0); also reproduces onmainorg.openrewrite.java.testing.junit5.MigrateJUnitTestCase