Hồi quy là lỗi xuất hiện sau khi thay đổi mã nguồn, mặc dù cùng chức năng đó đã hoạt động chính xác trước đây. Hồi quy có nghĩa là thay đổi mới đã “làm hỏng” những gì đã được viết và kiểm thử trước đó. Đây là một trong những vấn đề phổ biến và nguy hiểm nhất trong phát triển: khi sửa một lỗi, lập trình viên có thể vô tình làm hỏng ba chức năng khác. Theo Capers Jones Software Engineering 2023, mật độ trung bình của lỗi hồi quy là 1–3 trên mỗi 100 dòng mã đã thay đổi. Hãy cùng tìm hiểu nguyên nhân của hồi quy, phương pháp phát hiện và chiến lược phòng ngừa.
Những điểm chính
Hồi quy là tình huống khi chức năng đã hoạt động ở phiên bản trước ngừng hoạt động sau khi thực hiện thay đổi. Thay đổi có thể là bất cứ điều gì: sửa lỗi, thêm tính năng mới, tái cấu trúc, cập nhật thư viện hoặc thậm chí thay đổi cấu hình. Hồi quy là kẻ thù chính của sự ổn định: mỗi thay đổi đều có nguy cơ làm hỏng thứ đã được kiểm chứng và phát hành.
Thuật ngữ này xuất phát từ kiểm thử: kiểm thử hồi quy là quá trình chạy lại các kiểm thử hiện có sau mỗi thay đổi để xác minh rằng chức năng cũ không bị hỏng. Nếu kiểm thử trước đây đạt mà nay thất bại, thì đã xảy ra hồi quy. Theo nghĩa rộng hơn, hồi quy không chỉ là thất bại kiểm thử mà còn là bất kỳ sự suy giảm hành vi nào được người dùng hoặc QA nhận thấy. Theo Tricentis State of Testing 2023, hồi quy chiếm 35–45% tổng số lỗi được tìm thấy trong production.
Điều phân biệt hồi quy với lỗi thông thường là bối cảnh thời gian: lỗi thông thường có thể đã tồn tại từ đầu, trong khi hồi quy luôn là kết quả của một thay đổi. Sự khác biệt này rất quan trọng vì việc tìm nguyên nhân hồi quy bắt đầu bằng cách phân tích những gì đã thay đổi giữa trạng thái “đang hoạt động” và “ngừng hoạt động”. Git bisect là công cụ tiêu chuẩn để tìm commit gây ra hồi quy.
Hồi quy cục bộ — thay đổi trong mô-đun A làm hỏng chức năng trong cùng mô-đun A. Ví dụ: lập trình viên viết lại hàm sắp xếp và nó ngừng xử lý mảng rỗng đúng cách. Hồi quy cục bộ dễ phát hiện và sửa nhất vì nguyên nhân và kết quả ở gần nhau.
Hồi quy từ xa — thay đổi trong mô-đun A làm hỏng chức năng trong mô-đun B, không được kết nối trực tiếp bằng mã nhưng được kết nối qua dữ liệu hoặc thời gian. Ví dụ: thay đổi lược đồ cơ sở dữ liệu trong mô-đun “Người dùng” làm hỏng báo cáo trong mô-đun “Phân tích” sử dụng cùng một bảng. Hồi quy từ xa là nguy hiểm nhất: lập trình viên không ngờ rằng thay đổi của mình sẽ ảnh hưởng đến mô-đun khác.
Hồi quy tác dụng phụ — thay đổi tác dụng phụ (ghi log, lưu cache, gửi thông báo) làm hỏng hành vi mong đợi. Ví dụ: lập trình viên thêm cache để tăng tốc hiệu suất, nhưng do cache cũ, người dùng thấy dữ liệu lỗi thời. Hồi quy tác dụng phụ khó bị phát hiện bằng kiểm thử tự động vì các tác dụng phụ thường không được kiểm thử bao phủ.
Hồi quy hiệu suất — mã nguồn tiếp tục hoạt động chính xác về mặt chức năng nhưng chậm hơn trước. Ví dụ: thuật toán mã hóa mới tạo ra cùng kết quả, nhưng thời gian thực thi tăng từ 2 ms lên 200 ms. Hồi quy hiệu suất không bị phát hiện bởi kiểm thử đơn vị thông thường — cần điểm chuẩn và phân tích hiệu năng.
| Loại hồi quy | Ví dụ | Phương pháp phát hiện |
|---|---|---|
| Cục bộ | Sắp xếp bị hỏng | Kiểm thử đơn vị |
| Từ xa | Thay đổi lược đồ DB | Kiểm thử tích hợp |
| Tác dụng phụ | Cache cũ | Kiểm thử E2E |
| Hiệu suất | Phản hồi chậm hơn | Điểm chuẩn |
Nguyên nhân đầu tiên là sự kết nối mã nguồn. Các mô-đun càng phụ thuộc lẫn nhau thì khả năng thay đổi ở một mô-đun gây ra hồi quy ở mô-đun khác càng cao. Các phản mẫu kinh điển: God Object (đối tượng làm mọi thứ), Shotgun Surgery (thay đổi một chỗ cần chỉnh sửa ở hàng chục chỗ), phụ thuộc vòng tròn. Giảm kết nối là vấn đề của kiến trúc: nguyên lý SOLID, Dependency Injection, kiến trúc hexagon.
Nguyên nhân thứ hai là thiếu kiểm thử cho chức năng đã thay đổi. Nếu mã nguồn không được kiểm thử bao phủ, lập trình viên chỉ biết về hồi quy từ QA hoặc người dùng. Theo Google Testing Blog, các dự án có độ bao phủ kiểm thử >75% có số lượng hồi quy ít hơn 5 lần so với các dự án có độ bao phủ <25%. TDD (Phát triển hướng kiểm thử) đảm bảo rằng kiểm thử được viết trước mã nguồn, chứ không phải “khi nào có thời gian”.
Nguyên nhân thứ ba là yếu tố con người. Lập trình viên không biết về chức năng liên quan, không hiểu tất cả các phụ thuộc hoặc đơn giản là đang vội. Nguyên nhân là thiếu chia sẻ kiến thức về cơ sở mã nguồn. Giải pháp: đánh giá mã với lập trình viên từ các mô-đun khác, lập trình cặp, tài liệu hóa kiến trúc. Bus factor của dự án tỷ lệ nghịch với số lượng quyết định kiến trúc được ghi chép lại.
Kiểm thử hồi quy là quá trình chạy lại các kiểm thử hiện có sau mỗi thay đổi để xác minh rằng chức năng cũ không bị hỏng. Đây là cách duy nhất để đảm bảo rằng thay đổi mới không làm ảnh hưởng đến mã nguồn hiện có. Không có kiểm thử hồi quy, mỗi bản phát hành là một canh bạc: lập trình viên hy vọng không làm hỏng gì nhưng không thể xác nhận.
Kiểm thử hồi quy thủ công là cách tiếp cận tốn kém nhất và kém hiệu quả nhất. Khi dự án phát triển, số lượng kịch bản kiểm thử hồi quy tăng tuyến tính, trong khi thời gian thực hiện thủ công tăng theo cấp số nhân. Sau 2–3 năm phát triển, kiểm thử hồi quy thủ công có thể mất 2–3 tuần, khiến việc phát hành thường xuyên trở nên bất khả thi. Giải pháp duy nhất là tự động hóa.
Kiểm thử hồi quy tự động được chia thành các cấp độ theo kim tự tháp kiểm thử:
Theo Google Testing Blog, tỷ lệ tối ưu là 70% kiểm thử đơn vị, 20% kiểm thử tích hợp, 10% E2E. Sai lệch khỏi tỷ lệ này làm giảm hiệu quả của kiểm thử hồi quy: quá nhiều kiểm thử E2E làm chậm đường ống, quá ít kiểm thử đơn vị khiến vi lỗi không được phát hiện.
Chiến lược đầu tiên là Hồi quy toàn bộ. Tất cả kiểm thử của dự án được chạy. Cách tiếp cận đáng tin cậy nhất nhưng cũng chậm nhất. Phù hợp cho các dự án nhỏ (tới 10.000 kiểm thử, thời gian chạy <30 phút). Đối với các dự án lớn, hồi quy toàn bộ có thể mất hàng giờ, khiến đường ống CI/CD không thực tế.
Chiến lược thứ hai là Hồi quy chọn lọc. Chỉ chạy các kiểm thử liên quan đến mã nguồn đã thay đổi. Đồ thị phụ thuộc mã nguồn được sử dụng để xác định mối quan hệ. Công cụ: Bazel (Google), Nx (JavaScript), sbt (Scala). Hồi quy chọn lọc tiết kiệm 60–80% thời gian chạy nhưng yêu cầu xây dựng đồ thị phụ thuộc chính xác — sai sót dẫn đến bỏ sót hồi quy.
Chiến lược thứ ba là Hồi quy ưu tiên. Tất cả kiểm thử được xếp hạng theo mức độ ưu tiên: đường dẫn quan trọng (kịch bản người dùng quan trọng nhất), rủi ro cao (mã nguồn có lịch sử lỗi), mã nguồn đã thay đổi (mã nguồn bị ảnh hưởng bởi thay đổi). Các kiểm thử ưu tiên cao nhất được chạy trước — nếu chúng đạt, lập trình viên nhận được phản hồi nhanh. Chạy có giới hạn thời gian: kiểm thử quan trọng được kiểm tra trong 10 phút, phần còn lại chạy nền.
Bước đầu tiên và quan trọng nhất là văn hóa viết kiểm thử. Mỗi thay đổi nên đi kèm với một kiểm thử xác minh thay đổi hoạt động và một kiểm thử xác minh không có gì bị hỏng. TDD (Phát triển hướng kiểm thử) cho kết quả tốt nhất: lập trình viên viết kiểm thử thất bại trước, sau đó viết mã nguồn để vượt qua nó. Điều này đảm bảo kiểm thử tồn tại trước mã nguồn.
Bước thứ hai là đường ống CI/CD với thực thi kiểm thử bắt buộc. Pull request không thể được hợp nhất cho đến khi tất cả kiểm thử đạt. Không thể “bỏ qua” kiểm thử vì khẩn cấp — các thay đổi khẩn cấp trải qua bộ kiểm thử được tăng tốc nhưng bắt buộc. Theo Google DevOps Research, các nhóm có CI/CD bắt buộc có số lượng hồi quy trong production ít hơn 3 lần.
Bước thứ ba là giám sát production. Ngay cả kiểm thử tốt nhất cũng không đảm bảo bảo vệ 100% khỏi hồi quy. Các công cụ quan sát (Sentry, Datadog, New Relic) nên theo dõi các chỉ số chính sau mỗi lần triển khai: tỷ lệ lỗi, độ trễ, thông lượng. Tự động rollback khi vượt ngưỡng là lưới an toàn nếu hồi quy đến được production.
Bước thứ tư là đánh giá mã với tư duy hồi quy. Người đánh giá nên tự hỏi: “Những mô-đun nào khác có thể bị hỏng do thay đổi này?”. Kiểm tra mã nguồn đúng là chưa đủ — cần kiểm tra rằng nó không làm ảnh hưởng đến chức năng liên quan. Danh sách kiểm tra đánh giá mã nên bao gồm mục “kiểm tra hồi quy ở các mô-đun liên quan”.
Các câu hỏi thường gặp
Hồi quy là lỗi không tồn tại trước đây. Lỗi thông thường có thể đã tồn tại từ khi tính năng được tạo ra. Hồi quy luôn gắn liền với một thay đổi cụ thể — điều này cho phép sử dụng git bisect để tìm nguyên nhân.
Sử dụng git bisect: chỉ định commit nơi mọi thứ hoạt động và commit nơi nó bị hỏng. Git thực hiện tìm kiếm nhị phân trong lịch sử và tìm commit gây ra hồi quy. Điều này hoạt động ngay cả với các dự án lớn có hàng nghìn commit.
Không có con số chính xác, nhưng có một quy tắc thực nghiệm: độ bao phủ của luồng người dùng chính phải là 100%, độ bao phủ của tất cả chức năng ít nhất là 70%. Chất lượng quan trọng hơn số lượng: một kiểm thử kiểm tra trường hợp biên có giá trị hơn mười kiểm thử trên luồng chính.
Có, và điều này được gọi là hồi quy hạ tầng. Cập nhật hệ điều hành, thay đổi phiên bản cơ sở dữ liệu, cập nhật chứng chỉ SSL hoặc thay đổi cấu hình máy chủ web có thể làm hỏng mã nguồn đang hoạt động. IaC (Hạ tầng dưới dạng mã) và kiểm thử hạ tầng (Test Kitchen, Terratest) giúp phát hiện các hồi quy như vậy.
Bắt đầu với một luồng người dùng quan trọng. Viết kiểm thử tự động cho kịch bản quan trọng nhất (đăng nhập, thanh toán). Trình diễn cách kiểm thử phát hiện hồi quy. Khi nhóm thấy lợi ích, hãy mở rộng dần độ bao phủ.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm