Uber moved more than 75,000 test classes from <strong>JUnit 4</strong> to <strong>JUnit 5</strong> by using automated code transformation, and that matters...
Uber’s 75,000+ JUnit 4 to JUnit 5 Migration: Automated Code Transformation Done Right
Uber moved more than 75,000 test classes from JUnit 4 to JUnit 5 by using automated code transformation, and that matters because big migrations fail when teams rely on hand edits and wishful thinking. I’m Ermenildo Valdez Castro, Jr., and I’ve seen enough messy test suites to know this: the tooling matters, but the discipline behind it matters more.
Key Takeaways- Automated code transformation reduces the pain of large framework upgrades.
- JUnit 5 brings better extension support, clearer structure, and more modern test design.
- Big migrations still need human review, careful rollout, and solid CI checks.
- Most teams fail by underestimating edge cases, not by misunderstanding the framework.
What is Uber Migrates 75,000+ Test Classes from Junit 4 to Junit 5 Using Automated Code Transformation?
This is a large-scale test framework migration.
Uber took a huge pile of JUnit 4 tests and converted them to JUnit 5 with automated transformation tools, instead of rewriting each class by hand, which would have been slow, brittle, and frankly painful. JUnit 5 is not just “new JUnit”; it is a split design with the Jupiter programming model, the Platform for running tests, and Vintage support for older tests, as described in the official docs at JUnit 5 User Guide.
Here’s the point.
When I worked on large test suites, the real enemy was not the code itself, but the weight of stale patterns, weird imports, and old helper methods that nobody wanted to touch. Automated transformation helps because it handles the boring parts first, like annotation swaps, import changes, and method name updates, while engineers focus on behavior, coverage, and the ugly corners that tools miss.
JUnit 4 and JUnit 5 differ in more than syntax. JUnit 5 gives you stronger extension points, better parameterized tests, nested tests, repeated tests, and more control over test lifecycle. The design is cleaner, and that matters when your test suite feeds a CI system that has to stay fast and honest. If you want the official migration notes, the JUnit team has a direct guide at Migrating from JUnit 4.

The deeper lesson is simple.
A migration at Uber’s scale is not just a code task, it is a stewardship task, because test code is part of the company’s duty to ship safe software with less waste, less rework, and more respect for the people who depend on it. That is the part many teams miss when they treat tests like chores instead of guardrails.
Core Benefits/Importance
Automated migration sounds dull.
It is not dull when you have 75,000 classes on the line, because the benefits spread across velocity, safety, and sanity, and you can measure them in fewer broken builds, fewer manual edits, and fewer late-night fixes that make everyone grumpy.
- Speed at scale: I’ve watched teams stall for months because they tried to do framework upgrades one file at a time. Automated transformation cuts that dead weight and gets you to a usable baseline faster.
- Less human error: Hand edits lead to missed imports, broken annotations, and half-finished classes. A transformer can apply the same rule 10,000 times without getting tired or sloppy.
- Cleaner test structure: JUnit 5 supports nested tests, display names, parameterized tests, and extensions in a way that makes tests easier to read and maintain.
- Better CI hygiene: A modern test platform can improve reporting and make failures easier to trace. That saves time for engineers and protects release quality.
- Lower long-term cost: Old frameworks create drag. You keep paying interest on outdated code until you clean it up.
Most guides talk about “modernization” like it is some shiny parade.
The truth is, modernization only helps if it reduces waste and confusion, and if it respects the people doing the work. I learned that the hard way in big-company codebases, where a bad migration can turn into a support nightmare for dozens of teams.
At Google, Microsoft, Amazon, and Zulily, I saw the same pattern: teams who invested in tooling and rules moved faster later, while teams who clung to manual cleanup spent weeks arguing over tiny changes. That’s not theory. That is what happens when you ignore the dull stuff.
For a broader view on test reliability and CI, see Martin Fowler’s Test Pyramid. It is old advice, but it still beats cargo cult rituals.
Step-by-Step Implementation/Guide
A migration like this needs order.
You do not just run a magic script and call it a day, because automated transformation is a first pass, not a final judge, and the work only stays sane if you break it into clear steps, review the diffs, and keep the build green the whole time.
- Inventory the test suite
- Count test classes, note framework usage, and find odd cases like custom runners or legacy rules.
- I always start here, because without a map you are just guessing, and guessing is how teams waste two weeks on avoidable surprises.
- Pick the transformation rules
- Define what gets changed automatically: annotations, imports, assertions, lifecycle methods, and package updates.
- Uber likely had to protect edge cases too, because large codebases always hide strange helpers in one or two corners that nobody remembers until they break.
- Run the transformer in batches
- Don’t blast the whole repo at once unless you enjoy pain.
- Small batches make it easier to spot bad rewrites and keep the review queue from turning into a swamp.
- Use CI as a referee
- Run tests after each batch.
- Fail fast, then fix the rules instead of papering over broken behavior.
- Review risky cases by hand
- Custom runners, reflection tricks, and weird test fixtures need human eyes.
- Tools can move syntax; they cannot always understand intent.
- Update shared utilities
- Migrate helper libraries, test base classes, and internal wrappers so they fit the new framework.
- This part is where teams often miss hidden coupling.
- Measure coverage and failure patterns
- Watch for tests that changed behavior, especially around lifecycle and parameter injection.
- If failures cluster in one area, your transformation rules probably need a tweak.
- Keep the rollout boring
- Boring is good.
- The best migration is the one nobody remembers because it did not blow up prod or stall every team meeting.
I’ve done enough systems work to say this plainly: the hard part is not writing the script, it is proving that the script respects real code. That is where solid engineering shows up, the same way a good steward proves care by doing the small things well.
If you are planning a similar upgrade, read the official JUnit docs at JUnit 5 User Guide, and check Java migration tooling such as OpenRewrite at OpenRewrite. OpenRewrite is especially useful when you need repeatable source changes across many repositories.

Comparison Table
Here is the plain comparison.
| Topic | JUnit 4 | JUnit 5 |
|---|
| Core model | Old runner/rule model | Split into Platform, Jupiter, Vintage |
| Annotations | @Before, @After, @Test | @BeforeEach, @AfterEach, @Test |
| Parameterized tests | Extra add-ons needed | Built-in support |
| Extensions | Limited | Stronger extension model |
| Nested tests | Awkward | Supported |
| Migration help | Legacy baseline | Modern tooling and docs |
| Long-term fit | Aging code style | Better for new work |
The biggest competitor here is not another test framework.
It is staying with JUnit 4 and pretending the cost will somehow shrink on its own, which is the usual excuse people give when they are scared of cleanup. I’ve heard that speech more times than I care to count.
JUnit 4 is stable, sure.
Stable is not the same as healthy.
JUnit 5 gives teams a better shape for modern Java testing, and that becomes more valuable as codebases grow and teams split across services. For background on the platform pieces, the official JUnit project page is at junit.org/junit5.
Common Mistakes to Avoid
Most migration advice is too cheerful.
That is a problem, because the failures are usually boring and predictable, and if nobody says them out loud, teams will repeat the same mess with a fresh coat of paint.
- Running the tool with no review: Automated changes can be wrong in subtle ways, especially around lifecycle methods and custom test infrastructure.
- Ignoring custom runners and rules: These old JUnit 4 patterns often need manual work or special handling.
- Changing syntax but not behavior: A test can compile and still assert the wrong thing.
- Skipping CI validation: If you do not run the suite often, you will not know which batch caused the break.
- Forgetting shared test helpers: Base classes and utility methods can keep old assumptions alive long after the main tests are migrated.
- Treating migration as one giant dump: Small batches make rollback and diagnosis much easier.
Frankly, the most common mistake is pride.
Teams think they can “just do it” because the diff looks mechanical, then they discover a bunch of edge cases that were hiding in plain sight. I’ve been in those rooms, and the mood changes fast when the build goes red for no good reason.
You also need to think about people, not just code.
A good migration respects the engineers who inherit the results, because clean tests are a form of service to the whole team, not a trophy for the person who wrote the script. That sounds old-fashioned, but old-fashioned is often right.
Frequently Asked Questions
Why did Uber automate the JUnit 4 to JUnit 5 migration?
Because manual edits at that scale are slow, error-prone, and expensive. Automation gets the code to a safe baseline faster.
Is JUnit 5 backward compatible with JUnit 4?
Partly. The Vintage engine can run many JUnit 4 tests on the JUnit 5 platform, but full migration usually needs source updates.
What tools help with large Java migrations?
OpenRewrite is one common choice, along with custom scripts and CI checks. The right tool depends on the shape of the codebase.
What is the hardest part of this kind of migration?
Edge cases. Custom runners, test helpers, and hidden assumptions usually create the real work.
Final Thought
I like this migration because it rewards discipline.
It is not flashy, and it does not need hype, but it shows what good engineering looks like when a team treats code as something entrusted to them, not something to be thrown over the wall. I’m Ermenildo V Castro, and if there is a lesson here, it is simple: do the careful work, fix the boring parts, and respect the people who will live with the result.
The right tools matter.
So does the conscience behind the hands that use them.