How Uber Executed A JUnit Migration at Massive Scale

Bài tóm tắt Uber 2026 4. Điều chỉnh Độ khó: Trung bình

Uber · 2026 · 4. Điều chỉnh · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Monorepo Java của Uber chạy test bằng JUnit 4: hơn 600.000 test, 15 triệu dòng code. JUnit 4 ngừng phát triển từ 2021; JUnit 5 đã ra hơn tám năm; các bản JUnit sau định bỏ hỗ trợ JUnit 4. Bazel không hỗ trợ sẵn JUnit 5. Migrate tay thì mỗi developer phải học Jupiter API và tự chuyển test suite riêng.

Thay đổi chính

  • Nền trước: đưa mọi module test lên JUnit Platform, chạy cả test JUnit 4 (engine Vintage) lẫn JUnit 5 (Jupiter); viết test runner riêng làm lớp nối giữa rule test của Bazel và JUnit Platform (tìm test, listener, lọc test flaky). Từ đó team đã có thể viết JUnit 5 trước khi migration chạy.
  • OpenRewrite trên Bazel: OpenRewrite parse code thành LST (cây giữ cả kiểu, định dạng, metadata), recipe biến đổi trên cây rồi ghi lại. Viết một Bazel aspect gom file nguồn và classpath biên dịch của target, chạy recipe, ra file patch và các bảng CSV chẩn đoán.
  • Recipe tổng hợp: dựa trên recipe JUnit4to5 của OpenRewrite (Uber góp thêm cải tiến upstream), cộng recipe tự viết cho runner, rule và base class nội bộ; ưu tiên theo mức dùng thật đo bằng search recipe trên cả monorepo. Một precondition loại file dùng tiện ích chưa hỗ trợ, để không có file chuyển dở (trộn API JUnit 4 và 5 làm hỏng build) mà vẫn không bỏ cả target vì một file. Cuối recipe, FindDependencyUsage tìm dependency test mới để cập nhật cấu hình build.
  • Một script cho một target: chạy recipe, áp patch, sửa BUILD theo CSV dependency, build và chạy test; test hỏng thì dừng và báo; chỉ giữ thay đổi khi mọi test qua.
  • Rollout bằng Shepherd (công cụ thay đổi code diện rộng nội bộ): sinh diff cho từng target đã migrate thành công, CI kiểm thêm integration test, linter, coverage. Lượt đầu ra hơn 2.000 diff; sau đó lặp: đọc lỗi từ log, thêm hỗ trợ mẫu còn thiếu, chạy lại. File làm test hỏng thì trả về JUnit 4 (với hàng nghìn target, chẩn từng test là không thực tế); lỗi build lặp lại thì viết thêm recipe.
  • Kết quả: trong 4 tháng, hơn 75.000 class test, khoảng 1,25 triệu dòng thay đổi, hơn 5.000 diff tự động.
  • Về AI: thử dùng generative AI để chuyển nhiều file test một lúc nhưng không thành; AI chỉ dùng để gỡ lỗi build và test. Theo Uber, với quy mô và quy ước test riêng như vậy, cách tất định thực tế hơn. Tiếp theo dùng cùng cách cho Spring Boot 3, Guava → Java chuẩn, Joda-Time → java.time.

Vì sao

  • Migration framework không phải nâng dependency: cần một nền chạy được cả hai bản, công cụ biến đổi hiểu kiểu, và quy trình chỉ chấp nhận thay đổi khi test qua.

Bài học cho SWE

  • Dựng nền cho cả cũ lẫn mới cùng chạy trước, rồi mới chuyển code.
  • Đo mức dùng thật để chọn mẫu đáng tự động hoá; bỏ qua có chủ đích phần không hỗ trợ thay vì chuyển dở.
  • Cổng của mỗi thay đổi tự động là build + test của chính target đó, rồi thêm CI của tổ chức.
  • Lặp rollout: lượt đầu chỉ là bản đồ lỗi để viết thêm biến đổi.
  • Biến đổi tất định trên cây có kiểu vẫn thắng LLM ở codebase lớn có quy ước riêng (theo Uber, lúc viết).

Liên quan