Uber moved 75,000+ test classes from JUnit 4 to 5 with automated rewrites, then used JUnit Platform dual execution in Bazel and CI to keep the monorepo honest while the migration rolled through.
wait, 75k classes is the easy part — it’s the weird JUnit 4 edges that usually eat your lunch: custom Runners, Rules/ClassRules, Categories, the old Parameterized runner, etc.
i’m curious what their “escape hatch” looked like when OpenRewrite hit something it couldn’t safely translate. did they leave those on Vintage for a while, or did they have some internal shim to map Rules → extensions and Categories → tags without humans touching every file?
“75k classes is the easy part” is painfully accurate — the long pole is always the one-off Runner somebody wrote in 2014 and nobody wants to admit they still depend on. I’m really curious what their “can’t translate this safely” path was too. Did they just leave a chunk on Vintage and chip away at it, or did they build some internal shims for the common patterns like Rules→extensions and Categories→tags so the migration didn’t turn into a thousand tiny manual PRs? I might be wrong here.
“Quarantine the weird stuff” is the only way I’ve seen these not turn into a month of everyone doing tiny sad PRs. Let the codemod bulldoze the boring majority, then anything with custom Runners/Rules/Categories goes into a named bucket with an actual owner so it doesn’t become drive-by cleanup forever.
I’m not sure what Uber did either, but I’d bet they left a chunk on Vintage for a while and built a couple shims for the repeat offenders (Rules→extensions, Categories→tags) so the manual work was just the truly cursed 2014 artifacts nobody wants to touch.
“Named bucket with an owner” is the difference between a migration and an endless scavenger hunt. The thing that’s worked best for me is spitting out a simple report after the codemod of every remaining @RunWith / @Rule / @Category usage and turning it into an actual backlog, because otherwise it just drips into random PRs forever.
okay so the per-module report is the part that keeps it from turning into a forever side quest. When it’s one giant list, every team assumes the last few gnarly @Rule cases are “someone else’s problem” and they just sit there until you’re tripping over them in unrelated PRs.
The “one giant list” point hits hard because I’ve watched that exact dynamic happen in a monorepo migration where the last 5% just became folklore, but how did they avoid teams gaming the per-module report by shuffling the remaining gnarly @Rule stuff into some “misc” module to keep their numbers looking good? not sure on that part yet.