Uber successfully migrated over 75,000 test classes and 1.25 million lines of code from JUnit 4 to JUnit 5 using automated code transformation tools. This technical analysis examines the approach, challenges, and solutions in one of the largest testing framework migrations in the industry.
Uber's Large-Scale JUnit 4 to JUnit 5 Migration: Technical Analysis of 75,000+ Test Classes Transformation
In April 2026, Uber announced the completion of a massive migration effort, transforming over 75,000 test classes and more than 1.25 million lines of code from JUnit 4 to JUnit 5 across their Java monorepo. This migration represents one of the largest testing framework transitions in the industry, implemented through sophisticated automated code transformation and orchestration tooling.
Technical Background: The Imperative for Migration
JUnit 4 has been in maintenance mode since 2021, with no new features or significant improvements. For organizations like Uber, continuing with a legacy testing framework limits access to modern capabilities and contributes to technical debt. JUnit 5, by contrast, introduces a modular architecture built on the JUnit Platform with support for the Jupiter engine and improved parameterized testing.
The migration was driven by several technical factors:
- Extensibility: JUnit 5's modular architecture allows for better extensibility compared to JUnit 4's monolithic design
- Parameterized Testing: JUnit Jupiter provides more sophisticated parameterized testing capabilities
- Lambda Support: Native support for lambdas in JUnit 5 enables more expressive test code
- Dynamic Tests: Enhanced support for dynamic test generation
- Reduced Technical Debt: Moving away from a framework in maintenance mode
However, the migration presented significant challenges at Uber's scale. Their monorepo includes hundreds of thousands of tests integrated with Bazel, which does not natively support JUnit 5. The sheer volume of code—75,000+ test classes—made manual migration impractical and error-prone.
Technical Approach: Building a Migration Foundation
The Uber team approached the migration systematically, establishing a technical foundation that would enable incremental, safe transformation.
JUnit Platform Compatibility Layer
The first technical challenge was enabling JUnit 5 support in Uber's Bazel-based build system. The solution was to implement a unified execution model using the JUnit Platform, which supports both JUnit 4 and JUnit 5 tests simultaneously via the Vintage and Jupiter engines.
This compatibility layer was critical because it allowed:
- Coexistence: JUnit 4 and JUnit 5 tests could run alongside each other during the transition
- Incremental Migration: Teams could migrate at their own pace without disrupting workflows
- Validation: Each migrated test could be validated against the original behavior before complete migration
- Rollback Capability: Issues could be addressed without completely abandoning the migration
The JUnit Platform serves as the foundation that enables different testing engines to work together, providing a common interface for test discovery and execution.
OpenRewrite for Automated Transformation
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.
Key aspects of the OpenRewrite implementation included:
- Semantic Understanding: Unlike pattern-based tools, OpenRewrite understands code structure and relationships
- Deterministic Transformations: Consistent results across the entire codebase
- Extensibility: Custom transformations could be added for Uber-specific patterns
- Version Control Integration: Transformations could be applied incrementally with proper version control
The team defined transformation recipes to update annotations, replace legacy rules, and convert parameterized test patterns to JUnit Jupiter constructs. For example:
@Testannotations remained largely the same but gained new capabilities@Beforeand@Afterannotations were replaced with@BeforeEachand@AfterEach@Parameterizedannotations were replaced with JUnit Jupiter's parameterized testing- Exception testing patterns were updated to use
Assertions.assertThrows()
Custom Transformations for Uber-Specific Patterns
Uber's codebase included custom test runners and base classes that required special handling. The team extended the transformation recipes with custom transformations targeting these patterns:
- Uber Test Runners: Specific annotations and patterns used in Uber's testing infrastructure
- Base Classes: Common test base classes with JUnit 4-specific logic
- Utility Methods: Helper methods in test classes that used JUnit 4 APIs
These custom transformations ensured that the migration would be comprehensive and not limited to standard JUnit patterns.
Precondition Checks and Validation
To ensure migration quality, the team implemented several validation mechanisms:
- Precondition Checks: Automated verification that a test class was ready for migration
- Pattern Exclusion: Identification of unsupported patterns that required manual intervention
- Partial Migration Prevention: Mechanisms to avoid partially migrated test files
- Behavioral Validation: Automated verification that migrated tests produced identical results
The team also analyzed usage patterns across the codebase to prioritize high-frequency constructs, improving automation coverage and efficiency.
Implementation: The Shepherd Orchestration System
Managing transformations across 75,000+ test classes required sophisticated orchestration. Uber developed an internal system called Shepherd to manage the migration at scale.
Shepherd's Architecture
Shepherd provided several critical capabilities:
- Parallel Processing: Application of transformations across thousands of Bazel targets simultaneously
- Diff Generation: Creation of code diffs for review and version control
- Validation Pipelines: Integration with continuous integration systems for validation
- Progress Tracking: Monitoring of migration progress across the organization
Incremental Rollout Model
The migration followed an iterative rollout model:
- Initial Runs: Small-scale transformations to validate the approach
- Failure Analysis: Identification of patterns causing build or test failures
- Recipe Refinement: Updates to transformation logic based on failure analysis
- Scale Expansion: Gradual increase in migration scope as confidence grew
This iterative approach allowed the team to identify and address edge cases systematically, improving the automation's reliability over time.
Validation and Quality Assurance
Each transformation underwent rigorous validation:
- Unit Testing: Verification of individual transformation recipes
- Integration Testing: Testing of transformations in realistic contexts
- Behavioral Testing: Execution of migrated tests to ensure identical behavior
- Performance Testing: Verification that transformations didn't introduce performance regressions
The validation pipeline ensured that only thoroughly tested transformations were applied to the production codebase.
Technical Results and Implications
The migration achieved several significant outcomes:
- Complete Migration: All 75,000+ test classes successfully migrated to JUnit 5
- Minimal Manual Intervention: Less than 5% of migrations required manual changes
- No Behavioral Changes: All tests maintained identical behavior post-migration
- Performance Improvements: Some tests showed improved execution speed due to JUnit 5 optimizations
- Extensibility Gains: Teams began leveraging JUnit 5's advanced features immediately
Technical Debt Reduction
The migration reduced technical debt in several ways:
- Framework Modernization: Moving from a maintenance-mode framework to an actively developed one
- API Consistency: Standardization on modern testing APIs across the organization
- Knowledge Refresh: Updated team knowledge of current testing best practices
- Toolchain Integration: Better integration with modern development tools
Foundation for Future Transformations
Perhaps most importantly, the migration established a foundation for large-scale transformations using OpenRewrite. Uber engineers noted that the approach could be applied to other modernization efforts:
- Spring Boot 3 Migration: Integration into Bazel for Spring Boot 3 builds
- Guava Modernization: Migration from Guava to standard Java APIs
- Date/Time API Migration: Transition from Joda-Time to java.time
- Other Framework Upgrades: Potential for similar approaches to other library migrations
Technical Challenges and Solutions
The migration presented several significant technical challenges:
Challenge 1: Bazel Compatibility
Problem: Bazel does not natively support JUnit 5.
Solution: Implementation of a JUnit Platform compatibility layer that allowed both JUnit 4 (Vintage engine) and JUnit 5 (Jupiter engine) tests to run simultaneously.
Challenge 2: Scale and Complexity
Problem: 75,000+ test classes across a monorepo with interdependencies.
Solution: Development of the Shepherd orchestration system for parallel processing and validation.
Challenge 3: Custom Patterns
Problem: Uber-specific test runners and base classes not covered by standard transformations.
Solution: Extension of OpenRewrite recipes with custom transformations for Uber-specific patterns.
Challenge 4: Deterministic Transformations
Problem: Generative AI produced inconsistent results across custom test patterns.
Solution: Use of OpenRewrite's semantic code understanding for deterministic transformations.
Future Directions
The migration has opened several avenues for future work:
- Enhanced Automation: Further automation of edge cases and patterns
- Performance Optimization: Leveraging JUnit 5's performance improvements
- Advanced Feature Adoption: Exploitation of JUnit 5's advanced testing capabilities
- Toolchain Integration: Better integration with IDEs and other development tools
Conclusion
Uber's migration from JUnit 4 to JUnit 5 represents a significant technical achievement in large-scale software modernization. By combining the JUnit Platform's compatibility layer, OpenRewrite's semantic transformations, and Shepherd's orchestration capabilities, the team successfully migrated 75,000+ test classes with minimal manual intervention.
The approach demonstrates that even large, complex codebases can be systematically modernized through the combination of appropriate tools and processes. The migration not only updated the testing framework but also established a foundation for future modernization efforts across Uber's codebase.
As organizations continue to grapple with technical debt and legacy systems, Uber's experience provides a valuable blueprint for large-scale framework migrations that balance thoroughness with efficiency.
This technical analysis shows that while the challenges of migrating a testing framework at Uber's scale are substantial, they can be addressed through systematic approaches, appropriate tooling, and careful orchestration. The result is not just an updated testing framework, but a more maintainable, extensible, and modern testing infrastructure ready to support the organization's future development needs.
For more information about JUnit 5, see the official documentation.

Comments
Please log in or register to join the discussion