Uber's Large-Scale JUnit 4 to JUnit 5 Migration: Technical Analysis of 75,000+ Test Classes Transformation
#Dev

Uber's Large-Scale JUnit 4 to JUnit 5 Migration: Technical Analysis of 75,000+ Test Classes Transformation

Infrastructure Reporter
7 min read

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:

  1. Extensibility: JUnit 5's modular architecture allows for better extensibility compared to JUnit 4's monolithic design
  2. Parameterized Testing: JUnit Jupiter provides more sophisticated parameterized testing capabilities
  3. Lambda Support: Native support for lambdas in JUnit 5 enables more expressive test code
  4. Dynamic Tests: Enhanced support for dynamic test generation
  5. 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:

  1. Coexistence: JUnit 4 and JUnit 5 tests could run alongside each other during the transition
  2. Incremental Migration: Teams could migrate at their own pace without disrupting workflows
  3. Validation: Each migrated test could be validated against the original behavior before complete migration
  4. 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:

  1. Semantic Understanding: Unlike pattern-based tools, OpenRewrite understands code structure and relationships
  2. Deterministic Transformations: Consistent results across the entire codebase
  3. Extensibility: Custom transformations could be added for Uber-specific patterns
  4. 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:

  • @Test annotations remained largely the same but gained new capabilities
  • @Before and @After annotations were replaced with @BeforeEach and @AfterEach
  • @Parameterized annotations 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:

  1. Uber Test Runners: Specific annotations and patterns used in Uber's testing infrastructure
  2. Base Classes: Common test base classes with JUnit 4-specific logic
  3. 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:

  1. Precondition Checks: Automated verification that a test class was ready for migration
  2. Pattern Exclusion: Identification of unsupported patterns that required manual intervention
  3. Partial Migration Prevention: Mechanisms to avoid partially migrated test files
  4. 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:

  1. Parallel Processing: Application of transformations across thousands of Bazel targets simultaneously
  2. Diff Generation: Creation of code diffs for review and version control
  3. Validation Pipelines: Integration with continuous integration systems for validation
  4. Progress Tracking: Monitoring of migration progress across the organization

Incremental Rollout Model

The migration followed an iterative rollout model:

  1. Initial Runs: Small-scale transformations to validate the approach
  2. Failure Analysis: Identification of patterns causing build or test failures
  3. Recipe Refinement: Updates to transformation logic based on failure analysis
  4. 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:

  1. Unit Testing: Verification of individual transformation recipes
  2. Integration Testing: Testing of transformations in realistic contexts
  3. Behavioral Testing: Execution of migrated tests to ensure identical behavior
  4. 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:

  1. Complete Migration: All 75,000+ test classes successfully migrated to JUnit 5
  2. Minimal Manual Intervention: Less than 5% of migrations required manual changes
  3. No Behavioral Changes: All tests maintained identical behavior post-migration
  4. Performance Improvements: Some tests showed improved execution speed due to JUnit 5 optimizations
  5. Extensibility Gains: Teams began leveraging JUnit 5's advanced features immediately

Technical Debt Reduction

The migration reduced technical debt in several ways:

  1. Framework Modernization: Moving from a maintenance-mode framework to an actively developed one
  2. API Consistency: Standardization on modern testing APIs across the organization
  3. Knowledge Refresh: Updated team knowledge of current testing best practices
  4. 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:

  1. Spring Boot 3 Migration: Integration into Bazel for Spring Boot 3 builds
  2. Guava Modernization: Migration from Guava to standard Java APIs
  3. Date/Time API Migration: Transition from Joda-Time to java.time
  4. 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:

  1. Enhanced Automation: Further automation of edge cases and patterns
  2. Performance Optimization: Leveraging JUnit 5's performance improvements
  3. Advanced Feature Adoption: Exploitation of JUnit 5's advanced testing capabilities
  4. 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

Loading comments...