Làm việc với code legacy: bắt đầu từ test mô tả hành vi hiện tại

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Gần như mọi lập trình viên đều sẽ gặp code legacy: hệ thống chạy lâu năm, ít tài liệu, không có test, ai cũng sợ động vào. Cám dỗ lớn nhất là viết lại từ đầu, nhưng thường đó là con đường rủi ro và tốn kém. Bài này giới thiệu cách tiếp cận thực tế hơn: dựng lưới an toàn bằng characterization test rồi cải thiện từng bước nhỏ.

Code legacy là gì?

Có một định nghĩa thực dụng: code legacy là code không có test, nên bạn không thể thay đổi nó một cách tự tin. Tuổi đời hay công nghệ cũ không phải điều quyết định. Chính thiếu lưới an toàn khiến mọi chỉnh sửa trở nên đáng sợ.

Vì sao không nên viết lại toàn bộ?

  • Code cũ chứa rất nhiều kiến thức nghiệp vụ và bản sửa lỗi tích lũy qua thời gian, mà bạn sẽ mất nếu viết lại.
  • Viết lại tốn thời gian dài trong khi hệ thống cũ vẫn phải tiếp tục được bảo trì.
  • Bản mới thường lặp lại đúng những lỗi mà bản cũ đã sửa.
  • Rủi ro "chạy không giống cũ" rất cao, khó chứng minh bản mới đúng.

Cải thiện dần dần theo từng phần thường an toàn và mang lại giá trị sớm hơn. Dĩ nhiên có trường hợp phải thay thế hẳn, nhưng nên làm từng phần có kiểm soát.

Characterization test là gì?

Đây là loại test ghi lại hành vi hiện tại của code, dù hành vi đó đúng hay sai. Mục tiêu không phải xác minh tính đúng đắn mà là cho bạn biết khi nào mình vô tình làm thay đổi hành vi. Nó là lưới an toàn để refactor.

Cách viết từng bước

  1. Chọn đoạn code bạn cần thay đổi (một hàm, một class).
  2. Viết test gọi nó với đầu vào cụ thể và đặt một khẳng định tạm sai, ví dụ kỳ vọng kết quả là null.
  3. Chạy test, nó sẽ thất bại và cho biết giá trị thực tế.
  4. Sửa kỳ vọng thành đúng giá trị thực tế vừa quan sát.
  5. Lặp lại với nhiều đầu vào khác nhau, đặc biệt các trường hợp biên và các nhánh điều kiện.
// Hàm cũ, ít ai hiểu rõfunction calcShipping(weight, zone, isMember) {var fee = 0;if (zone == 1) fee = 20000;else if (zone == 2) fee = 35000;else fee = 50000;if (weight > 5) fee += (weight - 5) * 4000;if (isMember) fee = fee * 0.9;return Math.round(fee);}// Characterization test: ghi lại hành vi quan sát đượctest('khu vực 1, 3kg, không thành viên', () => {expect(calcShipping(3, 1, false)).toBe(20000);});test('khu vực 2, 8kg, thành viên', () => {expect(calcShipping(8, 2, true)).toBe(42300);});test('khu vực không xác định rơi vào mức cao nhất', () => {expect(calcShipping(1, 99, false)).toBe(50000);});test('đúng ngưỡng 5kg không tính phụ phí', () => {expect(calcShipping(5, 1, false)).toBe(20000);});

Có test này, bạn có thể refactor hàm sang cách viết sạch hơn (dùng bảng tra cứu cho phí khu vực, hằng số có tên...) và chạy lại để chắc hành vi giữ nguyên. Nếu phát hiện hành vi hiện tại là lỗi, hãy ghi chú lại và quyết định sửa riêng sau khi có sự đồng ý, vì nghiệp vụ có thể đang phụ thuộc vào đó.

Khi code khó test: tạo "đường nối" (seam)

Code legacy thường gọi trực tiếp database, file, mạng hoặc dùng biến toàn cục, nên khó đưa vào test. Cách xử lý là tạo những điểm cho phép thay thế phần phụ thuộc mà không đổi hành vi:

  • Trích hàm (Extract Function): tách phần logic thuần ra khỏi phần gọi bên ngoài, rồi test hàm thuần.
  • Truyền phụ thuộc qua tham số thay vì gọi cứng (xem bài về dependency injection).
  • Bọc lời gọi ngoại vi: tạo hàm mỏng bao quanh truy vấn database để có thể thay bằng bản giả khi test.
  • Test ở mức cao hơn: nếu không thể test đơn vị nhỏ, viết test tích hợp hoặc test đầu cuối cho luồng chính để có lưới thô trước.
  • Snapshot / golden master: lưu lại đầu ra thực tế của một lượng lớn đầu vào làm chuẩn, rồi so sánh sau khi sửa. Hữu ích với đầu ra phức tạp như báo cáo.

Quy trình cải thiện an toàn

  1. Xác định phạm vi: chỉ đụng vào phần bạn cần sửa hoặc thêm tính năng, đừng cố dọn cả hệ thống.
  2. Dựng lưới an toàn: viết characterization test cho phần đó.
  3. Refactor từng bước nhỏ và chạy test sau mỗi bước.
  4. Thêm thay đổi mong muốn (tính năng, sửa lỗi) trên nền code đã dễ hiểu hơn, kèm test mới.
  5. Commit nhỏ, tách riêng commit refactor và commit đổi hành vi.
  6. Để lại nơi tốt hơn: mỗi lần đi qua, cải thiện một chút (nguyên tắc người hướng đạo: để nơi cắm trại sạch hơn lúc đến).

Hiểu code trước khi sửa

  • Đọc lịch sử Git (blame, log) để biết vì sao đoạn code tồn tại.
  • Hỏi những người từng làm việc với nó, hoặc người dùng nghiệp vụ.
  • Vẽ sơ đồ luồng dữ liệu, ghi lại giả định bạn rút ra.
  • Chạy dưới debugger để quan sát hành vi thật.
  • Nhớ rằng những đoạn xấu xí có thể xử lý trường hợp đặc biệt có thật mà bạn chưa biết. Chớ vội xóa.

Thái độ khi làm việc với code legacy

Đừng phán xét người viết trước. Họ đã làm việc với yêu cầu, hạn chót và hiểu biết tại thời điểm đó. Hãy coi đây là cơ hội học hỏi: nhìn thấy hậu quả thực tế của các lựa chọn thiết kế là bài học rất quý. Và hãy chia sẻ kiến thức bạn khám phá được bằng tài liệu hoặc test, để người sau không phải bắt đầu từ con số không.

Kết luận

Thay vì sợ hay viết lại tất cả, hãy bắt đầu từ việc ghi lại hành vi hiện tại bằng characterization test, rồi cải thiện từng bước nhỏ có lưới an toàn. Cách làm này vừa thực tế vừa giúp bạn hiểu sâu hệ thống.

Nếu bạn muốn rèn kỹ năng đọc, kiểm thử và cải thiện code qua các bài thực hành, hãy tham khảo các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment