What happens
MigrateJUnitTestCase deletes a TestCase(String) constructor whose body becomes empty once
super(testName) is removed, but nothing else in the compilation unit — or in any other file — is
updated. Any code that called that constructor is left pointing at something that no longer exists,
and the class fails to compile.
There are two shapes, and neither involves anything exotic.
Minimal reproduction 1 — caller in the same file
Before:
import junit.framework.TestCase;
public class MathTest extends TestCase {
public MathTest(String testName) {
super(testName);
}
public static void main(String[] args) {
new MathTest("FOO").testAdd();
}
public void testAdd() {
assertEquals(2, 1 + 1);
}
}
After org.openrewrite.java.testing.junit5.MigrateJUnitTestCase:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MathTest {
public static void main(String[] args) {
new MathTest("FOO").testAdd();
}
@Test
public void testAdd() {
assertEquals(2, 1 + 1);
}
}
error: constructor MathTest in class MathTest cannot be applied to given types;
required: no arguments
found: java.lang.String
The constructor was removed correctly. The call to it, six lines below in the same file, was not.
Minimal reproduction 2 — subclass calling the removed base constructor
Before:
import junit.framework.TestCase;
public abstract class BaseTest extends TestCase {
public BaseTest(String name) {
super(name);
}
}
public class MathTest extends BaseTest {
public MathTest(String name) {
super(name);
}
public void testAdd() {
assertEquals(2, 1 + 1);
}
}
After:
public abstract class BaseTest {
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MathTest extends BaseTest {
public MathTest(String name) {
super(name);
}
@Test
public void testAdd() {
assertEquals(2, 1 + 1);
}
}
BaseTest is migrated correctly. MathTest is untouched and now calls a constructor that no longer
exists:
error: constructor BaseTest in class BaseTest cannot be applied to given types;
Why this matters
Both shapes are hard compile breaks rather than silent behaviour changes, but that is little comfort
in a large migration: the recipe presents its output as a clean, compiling result and instead leaves
the caller side entirely untouched.
| Caller shape |
Constructor removed? |
Caller updated? |
Outcome |
Same-file new MathTest("FOO") |
Yes |
No |
does not compile |
Subclass super(name) one level down |
Yes (base only) |
No |
does not compile |
The removal stops at the first level of the hierarchy and never propagates past it, and it never
looks for in-file callers at all.
Why the existing tests don't cover it
TEST_CASE_SUPER_MATCHER only matches a super(...) whose resolved target is
junit.framework.TestCase itself:
private static final MethodMatcher TEST_CASE_SUPER_MATCHER =
new MethodMatcher("junit.framework.TestCase <constructor>(..)");
- In
BaseTest, super(name) resolves to TestCase(String), so it matches, the call is removed, the
body becomes empty, and the constructor is deleted by the check in visitMethodDeclaration.
- In
MathTest, super(name) resolves to BaseTest(String), so it does not match and nothing is
touched.
MigrateJUnitTestCaseTest.caseWithConstructorCallingSuperTestName covers the removal, but the
fixture contains nothing that calls the constructor. convertExtendedTestCase does cover a
two-level hierarchy (CTest extends TestCase, MathTest extends CTest), but neither class declares
a constructor, so the intersection of "hierarchy" and "constructors" is untested. That intersection
is reproduction 2 above.
A fix needs to check whether the constructor is still referenced before deleting it — rewriting the
call site to the no-arg form where possible — and propagate the removal down the hierarchy, which
also requires dropping the subclass's own now-redundant super(name) call and constructor. Note the
related-but-distinct case where the constructor is kept because its body had other statements,
which leaves a class JUnit 5 cannot instantiate — filed separately.
Environment
- rewrite-testing-frameworks
3.44.0 (tag v3.44.0); also reproduces on main
- Recipe:
org.openrewrite.java.testing.junit5.MigrateJUnitTestCase
What happens
MigrateJUnitTestCasedeletes aTestCase(String)constructor whose body becomes empty oncesuper(testName)is removed, but nothing else in the compilation unit — or in any other file — isupdated. Any code that called that constructor is left pointing at something that no longer exists,
and the class fails to compile.
There are two shapes, and neither involves anything exotic.
Minimal reproduction 1 — caller in the same file
Before:
After
org.openrewrite.java.testing.junit5.MigrateJUnitTestCase:The constructor was removed correctly. The call to it, six lines below in the same file, was not.
Minimal reproduction 2 — subclass calling the removed base constructor
Before:
After:
BaseTestis migrated correctly.MathTestis untouched and now calls a constructor that no longerexists:
Why this matters
Both shapes are hard compile breaks rather than silent behaviour changes, but that is little comfort
in a large migration: the recipe presents its output as a clean, compiling result and instead leaves
the caller side entirely untouched.
new MathTest("FOO")super(name)one level downThe removal stops at the first level of the hierarchy and never propagates past it, and it never
looks for in-file callers at all.
Why the existing tests don't cover it
TEST_CASE_SUPER_MATCHERonly matches asuper(...)whose resolved target isjunit.framework.TestCaseitself:BaseTest,super(name)resolves toTestCase(String), so it matches, the call is removed, thebody becomes empty, and the constructor is deleted by the check in
visitMethodDeclaration.MathTest,super(name)resolves toBaseTest(String), so it does not match and nothing istouched.
MigrateJUnitTestCaseTest.caseWithConstructorCallingSuperTestNamecovers the removal, but thefixture contains nothing that calls the constructor.
convertExtendedTestCasedoes cover atwo-level hierarchy (
CTest extends TestCase,MathTest extends CTest), but neither class declaresa constructor, so the intersection of "hierarchy" and "constructors" is untested. That intersection
is reproduction 2 above.
A fix needs to check whether the constructor is still referenced before deleting it — rewriting the
call site to the no-arg form where possible — and propagate the removal down the hierarchy, which
also requires dropping the subclass's own now-redundant
super(name)call and constructor. Note therelated-but-distinct case where the constructor is kept because its body had other statements,
which leaves a class JUnit 5 cannot instantiate — filed separately.
Environment
3.44.0(tagv3.44.0); also reproduces onmainorg.openrewrite.java.testing.junit5.MigrateJUnitTestCase