Thêm ví điện tử vào app, mã giảm giá lại hỏng: chuyện functional test và regression test
Tháng trước team mình thêm phương thức thanh toán ví điện tử vào app đặt đồ ăn. Tính năng mới chạy ngon lành, QA test kỹ từng bước, demo cho sếp cũng mượt. Hai ngày sau khi release, bộ phận chăm sóc khách hàng báo về: mã giảm giá cho đơn đầu tiên không áp dụng được nữa. Không phải cho thanh toán ví, mà cho cả thanh toán tiền mặt, thứ mà chẳng ai đụng vào.
Bài này mình viết lại chuyện đó, và cách nó làm mình hiểu rõ hơn sự khác nhau giữa hai loại test mà trước giờ team hay gộp chung làm một.
Chuyện gì đã xảy ra
Khi thêm ví điện tử, một bạn dev refactor lại hàm tính tổng tiền đơn hàng để gom các phương thức thanh toán về một chỗ. Trong lúc refactor, thứ tự tính toán bị đổi: phí giao hàng được cộng trước, rồi mới áp mã giảm giá. Mã "đơn đầu tiên giảm 30k cho đơn từ 100k" có điều kiện kiểm tra tổng tiền, và giờ nó kiểm tra trên con số đã bao gồm phí ship. Một số đơn đáng lẽ không đủ điều kiện lại được giảm, một số đơn khác thì bị tính sai số tiền giảm.
Toàn bộ test cho tính năng ví điện tử đều pass. Chúng kiểm tra đúng thứ chúng cần kiểm tra: chọn ví, chuyển sang app ví, nhận callback, cập nhật trạng thái đơn. Không có test nào hỏi câu "những thứ đang chạy ổn trước đây có còn chạy ổn không".
Hai câu hỏi khác nhau
Sau vụ đó mình ngồi tách ra cho rõ.
Functional test trả lời câu hỏi: tính năng này có làm đúng điều mà yêu cầu mô tả không? Với ví điện tử, đó là "người dùng chọn ví, thanh toán thành công, đơn chuyển sang trạng thái đã thanh toán". Mình viết functional test dựa trên đặc tả, đứng ở vị trí người dùng, không quan tâm code bên trong chia thành bao nhiêu hàm.
Regression test trả lời một câu hỏi khác: sau khi thay đổi, những gì đã từng đúng có còn đúng không? Nó không quan tâm tính năng mới. Nó quan tâm mã giảm giá, phí ship, lịch sử đơn hàng, mấy thứ đã chạy ổn suốt một năm.
Hai loại test có thể dùng chung kỹ thuật, thậm chí chung cả công cụ. Một functional test viết hôm nay sẽ trở thành một phần của bộ regression vào tuần sau. Nhưng mục đích khác nhau, nên cách chọn test để chạy cũng khác nhau. Mình thấy bài viết về functional vs regression testing này giải thích khá gọn phần khác biệt đó, team mình đã dùng nó làm tài liệu đọc chung trước buổi họp retro.
Team mình đã thay đổi gì
Tách bộ test theo mục đích
Trước đây toàn bộ test nằm chung một thư mục, chạy chung một lệnh. Giờ tụi mình chia ra:
- Functional test cho tính năng đang phát triển, chạy mỗi lần push lên nhánh feature.
- Regression test cho các luồng đã ổn định, chạy mỗi lần merge vào nhánh chính và chạy lại toàn bộ mỗi đêm.
Việc tách này nghe đơn giản, nhưng nó buộc mọi người phải nghĩ: test mình đang viết là để chứng minh tính năng mới đúng, hay để bảo vệ thứ cũ?
Liệt kê những luồng "không được phép hỏng"
Mình ngồi với PM và bạn bên CS, liệt kê những luồng mà nếu hỏng thì khách sẽ gọi điện ngay. Ra được 14 luồng: đăng nhập bằng số điện thoại, áp mã giảm giá, tính phí ship theo khoảng cách, đặt lại đơn cũ, hủy đơn trước khi quán nhận, vân vân. Mỗi luồng có ít nhất một regression test đi từ đầu đến cuối qua API thật, với database được seed sẵn.
Mã giảm giá đơn đầu tiên giờ có 6 test riêng, mỗi test một kiểu đơn khác nhau: dưới ngưỡng, đúng ngưỡng, trên ngưỡng, có phí ship cao, có phí ship bằng 0, và người dùng đã có đơn trước đó.
Mỗi bug sửa xong là một regression test
Quy tắc này team mình đặt ra từ trước nhưng ít ai làm. Giờ thì reviewer sẽ không approve PR sửa bug nếu không kèm test tái hiện lại bug đó. Sau ba tháng, bộ regression có thêm khoảng 40 test, phần lớn đến từ bug thật.
Chuyện công cụ
Phần tốn công nhất là viết và bảo trì regression test cho API. Các luồng như đặt đơn có payload khá lớn, mỗi lần API thay đổi một field là phải sửa cả chục file JSON.
Team đã thử vài cách. Ban đầu là viết tay bằng Jest với supertest. Sau đó thử cách ghi lại traffic thật trên môi trường staging rồi phát lại thành test (Keploy là một công cụ làm theo kiểu này), cách này hợp với những endpoint payload dài mà không ai muốn ngồi gõ tay. Những case biên như đơn đúng ngưỡng 100k thì vẫn phải viết tay, vì traffic thật hiếm khi rơi đúng vào đó.
Mình không nghĩ có một công cụ đúng cho mọi trường hợp. Quan trọng hơn là xác định được test đó thuộc loại nào, rồi mới chọn cách viết.
Sau ba tháng
Bộ regression đã chặn được 5 lỗi trước khi lên production. Ba trong số đó giống hệt vụ mã giảm giá: một thay đổi ở chỗ này làm hỏng một thứ ở chỗ khác mà người sửa không hề nghĩ tới. CI chạy lâu hơn khoảng 6 phút. Bù lại, mình không còn phải ngồi giải thích với CS vì sao một tính năng chẳng ai đụng vào lại tự dưng hỏng.
Nếu team bạn cũng đang để chung mọi test vào một chỗ, thử làm một việc nhỏ: liệt kê 10 luồng mà khách hàng sẽ phàn nàn đầu tiên nếu chúng hỏng, rồi kiểm tra xem luồng nào chưa có test đi từ đầu đến cuối. Danh sách đó thường ngắn hơn mình tưởng, và đáng sợ hơn mình tưởng.
All Rights Reserved