Uber engineers migrated more than 75,000 test classes and over 1.25 million lines of code from JUnit 4 to JUnit 5 across their Java monorepo using automated code transformation and orchestration tooling. The migration was driven by the need to adopt a modern testing framework with improved extensibility and to reduce technical debt associated with a legacy system in maintenance mode.
JUnit 4 has been in maintenance mode since 2021, while JUnit 5 introduces a modular architecture built on the JUnit Platform with support for the Jupiter engine and improved parameterized testing. For Uber, continuing with JUnit 4 limited access to newer capabilities made migration necessary despite the complexity introduced by scale and infrastructure constraints.
Anshuman Mishra and Kaushik Vejju of Uber noted,
Deterministic transformation tooling was critical for consistency at this scale.
Uber engineers noted that generative AI produced inconsistent results across custom test patterns. Uber’s monorepo includes hundreds of thousands of tests integrated with Bazel, which does not natively support JUnit 5. To address this, engineers first enabled a unified execution model using the JUnit Platform, allowing both JUnit 4 and JUnit 5 tests to run together via the Vintage and Jupiter engines. This compatibility layer ensured that migration could proceed incrementally without disrupting existing workflows.
/filters:no_upscale()/news/2026/04/uber-junit4-junit5-migration/en/resources/1junit5-1776546985000.jpeg)
Enabling JUnit 5 support for Bazel (Source: Uber Blog Post)
/filters:no_upscale()/news/2026/04/uber-junit4-junit5-migration/en/resources/1Screenshot 2026-04-18 at 1.46.18 PM-1776546985000.png)
With the execution foundation in place, Uber adopted OpenRewrite to automate source code changes. OpenRewrite operates on a semantic representation of code, enabling deterministic transformations from JUnit 4 APIs to JUnit 5 equivalents. Engineers defined transformation recipes to update annotations, replace legacy rules, and convert parameterized test patterns to JUnit Jupiter constructs.
To support internal testing patterns, the team extended these recipes with custom transformations targeting Uber-specific test runners and base classes. Precondition checks were introduced to avoid partially migrated test files and ensure that unsupported patterns were excluded from automated updates. Engineers also analyzed usage patterns across the codebase to prioritize high-frequency constructs, improving automation coverage and efficiency.
Execution at scale was managed through an internal orchestration system called Shepherd, which applied transformations across thousands of Bazel targets in parallel. Shepherd generated code diffs and validated them through continuous integration pipelines, including unit and integration test execution, ensuring behavioral correctness before changes were accepted.
/filters:no_upscale()/news/2026/04/uber-junit4-junit5-migration/en/resources/1Screenshot%202026-04-18%20at%201.46.18%E2%80%AFPM-1776546985000.png)
Automated diff generation through Shepherd (Source: Uber Blog Post)
The migration followed an iterative rollout model. Initial runs surfaced build and test failures, which informed updates to the transformation logic. Over successive iterations, automation coverage improved, enabling larger portions of the codebase to be migrated with minimal manual intervention.
Uber engineers said the migration established a foundation for large-scale transformations using OpenRewrite. Ongoing efforts include integrating it into Bazel for Spring Boot 3 builds and migrating Guava to standard Java APIs and Joda-Time to java.time.