Migrating from JUnit 4 to JUnit 5: Uber’s Strategic Leap Forward
In an impressive feat of engineering prowess, Uber has successfully migrated over 75,000 test classes and more than 1.25 million lines of code from JUnit 4 to JUnit 5 within their expansive Java monorepo. This ambitious undertaking was driven by a strong desire to leverage a modern testing framework with enhanced extensibility and to alleviate the technical debt accumulated from working with a legacy system that was in maintenance mode.
The Necessity of Migration
JUnit 4 has been in a state of maintenance since 2021, rendering it less suitable for the demands of contemporary software development. The introduction of JUnit 5 marked a significant evolution, offering a modular architecture built on the JUnit Platform, which includes support for the Jupiter engine and improved parameterized testing capabilities. For a company like Uber, sticking with JUnit 4 restricted access to these innovative features. Therefore, migration was imperative, despite the challenges posed by the sheer scale of operations and infrastructure constraints.
Quote Highlight
Anshuman Mishra and Kaushik Vejju from Uber emphasized the significance of the migration effort:
“Deterministic transformation tooling was critical for consistency at this scale.”
Challenges Faced During Migration
One notable challenge encountered by Uber engineers involved the use of generative AI, which often produced inconsistent results across custom test patterns. Given that Uber’s monorepo houses hundreds of thousands of tests integrated with Bazel—a build tool that does not natively support JUnit 5—the engineering team had to devise an innovative strategy.
Unified Execution Model
To facilitate a smooth transition, Uber’s engineers first established a unified execution model utilizing the JUnit Platform. This allowed both JUnit 4 and JUnit 5 tests to coexist and run in harmony through two different engines: Vintage for JUnit 4 tests and Jupiter for JUnit 5 tests. This compatibility layer was paramount as it enabled an incremental migration without disrupting existing workflows.
Enabling JUnit 5 support for Bazel (Source: Uber Blog Post)
Automating the Migration Process
With the foundational execution mechanism established, Uber turned to OpenRewrite to automate the necessary source code changes. OpenRewrite functions on a semantic representation of code, allowing for deterministic transformations from JUnit 4 APIs to their JUnit 5 equivalents. To facilitate this extensive task, engineers defined specific transformation recipes designed to:
- Update annotations
- Replace legacy rules
- Convert parameterized test patterns to JUnit Jupiter constructs
Custom Transformations
To further streamline the process, the team tailored these recipes with custom transformations aiming at unique Uber-specific test runners and base classes. They instituted precondition checks aimed at preventing the inclusion of partially migrated test files, ensuring that unsupported patterns were systematically excluded from automated updates. A thorough analysis of usage patterns across the codebase also facilitated prioritized migration of high-frequency constructs, greatly enhancing automation coverage and efficiency.
Orchestrating Execution at Scale
To effectively manage the scale of the migration process, Uber utilized an internal orchestration system known as Shepherd. This system enabled transformation applications across thousands of Bazel targets concurrently. Shepherd not only generated essential code diffs but also carried out validations through continuous integration pipelines, including unit and integration test executions. This critical step ensured behavioral correctness prior to any changes being finalized.
Automated diff generation through Shepherd (Source: Uber Blog Post)
An Iterative Rollout Model
Uber employed an iterative rollout model throughout the migration process. Initial runs unveiled various build and test failures, which provided valuable feedback that was used to refine and improve the transformation logic. As more iterations took place, the automation coverage expanded, allowing larger segments of the codebase to be migrated with minimal manual intervention.
Uber engineers highlighted that this migration not only modernized their testing framework but also established a robust foundation for future large-scale transformations using OpenRewrite. Current ongoing efforts include plans to integrate this powerful tool into Bazel for Spring Boot 3 builds, as well as migrating Guava to standard Java APIs and Joda-Time to java.time.
In summary, Uber’s comprehensive migration from JUnit 4 to JUnit 5 underscores the importance of leveraging modern testing frameworks to enhance software development processes. The structured, systematic approach adopted by the engineering team serves as a compelling case study for other organizations looking to modernize their codebases while minimizing disruption to ongoing development efforts.
Inspired by: Source



