Bộ test xanh lè, mà mở app lên thì chữ bị cắt đôi
Đợt viết E2E cho luồng onboarding rồi soi lại toàn bộ trang admin. Unit test của mình không sai, nó chỉ không bao giờ hỏi cái câu quan trọng nhất là ghép hết lại thì có ai đi qua nổi không.
Mở bài
Chào các bạn, Ở bài trước mình có kể chuyện định mở lớp dạy user dùng app, rồi bỏ luồng năm màn xuống còn ba màn. Bài này là đoạn tiếp theo, tức là lúc code xong rồi và phải trả lời câu hỏi làm sao biết nó chạy.
Đống unit test cho luồng đó có đủ. Controller có test, widget có test, chạy phát nào xanh phát đó. Nhìn vào thì yên tâm lắm.
Rồi mình cắm cái simulator vào, chạy luồng đó lên và chụp lại từng màn.
Màn thứ ba, chỗ giải thích bao giờ thì nhận được tiền, hiện ra đúng một dòng chữ như thế này: "Bạn mua hàng qu...".
Không có test nào đỏ cả. Vì chẳng có test nào biết đọc chữ trên màn hình.
Bài này là mấy thứ mình nhặt được sau đợt đó, cộng với một đợt khác nặng đô hơn là ngồi soi lại toàn bộ trang quản trị. Cả hai đều cho mình cùng một kết luận, là E2E không phải cái tầng cao nhất của kim tự tháp test, nó là thứ duy nhất chịu mở sản phẩm lên xem hộ mình.
Thân bài
1. Unit test hỏi "hàm này đúng chưa", E2E hỏi "có ai đi hết được không"
Hai câu này nghe gần nhau nhưng khác hẳn, và mình mất khá lâu mới tách bạch ra được.
Unit test lấy giả định của người viết code làm đầu vào, rồi kiểm tra xem code có trả ra đúng cái mà giả định đó mong đợi không. Nó rất giỏi bắt lỗi tính toán, lỗi biên, lỗi điều kiện. Cái nó không bao giờ hỏi là mấy giả định kia ngoài đời có ai đáp ứng cho không.
Mình đã dính đúng một quả như vậy ở bài trước, cái gate chống farm account có đủ hàm, đủ ngưỡng, đủ test xanh, chỉ thiếu mỗi việc là app chưa bao giờ gửi dữ liệu lên nên nó chưa chặn được ai bao giờ. Từng mảnh đều đúng, chỉ là không có sợi dây nào nối chúng lại.
Bug đắt nhất thường không nằm trong hàm nào cả, nó nằm ở khoảng trống giữa hai hàm đúng.
Khoảng trống đó là thứ duy nhất mà E2E nhìn thấy, vì nó là loại test duy nhất đi từ đầu này sang đầu kia mà không được phép giả định giữa đường.
2. Chạy trên máy thật rồi chụp màn, hai lỗi rơi ra ngay
Quay lại cái luồng onboarding. Mình viết một bài E2E đi hết ba màn, mỗi màn chụp một tấm, và cuối cùng khẳng định cái cờ đã xem hướng dẫn được ghi đúng một lần.
Chụp màn nghe như thừa. Nhưng đúng ba tấm ảnh đó lôi ra hai lỗi.
Lỗi thứ nhất là cái timeline bị cắt chữ ở trên. Chỗ đó mình tái sử dụng một widget có sẵn trong app, mà widget đó vốn thiết kế để đựng nhãn ngắn xếp ngang. Mình nhồi nguyên một câu dài vào, nó cắt cụt. Test widget vẫn xanh, vì findsOneWidget chỉ quan tâm cái widget có tồn tại không, chứ nó không quan tâm chữ bên trong có đọc được không.
Lỗi thứ hai đau hơn. Ở màn hai, nút "Mua ngay" hiện ra màu xám lè kiểu bị vô hiệu hoá, chỉ vì hàm xử lý được truyền vào là rỗng. Về mặt kỹ thuật thì hoàn toàn đúng ý, vì đây là màn hướng dẫn chứ không phải màn mua thật, không cho bấm là chuẩn. Nhưng người dùng có biết cái lý do đó đâu. Họ chỉ thấy ngay tại cái màn đang dạy họ dùng app thì cái nút to nhất trên màn hình đang hỏng.
Sửa thì đơn giản, giữ nguyên màu thật rồi bọc lại để chặn tương tác. Cái đáng nói là không một dòng test nào trong dự án có khả năng phát hiện ra chuyện này, kể cả nếu mình viết thêm trăm cái nữa.
Test khẳng định phần mềm làm đúng thứ mình bảo nó làm. Không cái nào khẳng định thứ đó trông ra hồn.
3. E2E không có nghĩa là phải gọi vào hệ thống thật
Đây là chỗ mình nghĩ nhiều người hiểu nhầm, và bản thân mình lúc đầu cũng định làm sai.
Phản xạ tự nhiên là E2E thì phải thật hết, gọi backend thật, đăng nhập tài khoản thật. Nhưng ngồi nghĩ kỹ thì luồng onboarding chỉ hiện ra với người chưa xem hướng dẫn bao giờ. Muốn test thật thì mỗi lần chạy phải kiếm đâu ra một tài khoản trinh nguyên, hoặc là ngồi sửa cờ trong database production. Cả hai đều tệ.
Nên cuối cùng mình cho giả lớp gọi API, còn toàn bộ phần còn lại là thật. Màn thật, widget thật, điều hướng thật, chạy trên máy thật.
Nghĩ lại thì cái từ end-to-end nó không nói rõ là đầu nào tới đầu nào. Ở đây mình chọn đầu này là ngón tay người dùng, đầu kia là cái cờ được ghi xuống. Đoạn đường dây mạng nằm ngoài phạm vi đó, và cố nhét nó vào chỉ làm bài test vừa chậm vừa hay đỏ oan.
Một điểm cộng nữa là bộ công cụ integration test nằm sẵn trong SDK của Flutter, không phải cài thêm gì. Cái giá phải trả gần như bằng không, nên chuyện đắn đo có đáng viết hay không hoá ra là mình tự nghĩ ra.
4. Chi phí thật của E2E nằm ở mấy chỗ không ai kể
Viết phần logic của bài test thì nhanh. Thời gian của mình đi vào hai chỗ chẳng liên quan gì tới nghiệp vụ cả.
Chỗ thứ nhất, luồng onboarding kết thúc bằng lệnh chuyển sang màn dashboard và xoá sạch stack. Trong môi trường test thì cái màn dashboard đó không tồn tại, nên lệnh chờ giao diện ổn định cứ treo im cho tới lúc hết giờ. Chẳng có lỗi gì hiện ra, chỉ là đứng đó. Phải khai báo một cái dashboard giả rỗng tuếch thì mới qua.
Chỗ thứ hai, trên iOS thì muốn chụp được màn hình phải chuyển bề mặt vẽ sang dạng ảnh trước đã, không thì lệnh chụp không ra gì.
Cả hai đều là kiểu kiến thức mà không tài liệu nào chịu ghi ở chỗ dễ thấy, mình phải tự đâm đầu vào rồi mới biết. Mình nghĩ đây mới là lý do thật khiến nhiều dự án không có E2E, chứ không phải vì viết khó. Cái khó nằm ở lần đầu tiên dựng cho nó chạy được. Lần thứ hai trở đi thì rẻ hẳn.
5. Loại E2E rẻ nhất lại chẳng cần dựng gì
Bên backend mình có một bài test mà đặt tên là E2E, nhưng nó không cần database, không cần Redis, không cần khởi động lại cái gì.
Bối cảnh thế này. Đối tác gửi về một cái payload báo có đơn hàng thành công. Cái payload đó đi qua bộ nhận, rồi qua bộ ánh xạ dữ liệu, rồi ra một kết quả gồm mã sàn, mã đơn, mã tracking của người dùng và số tiền hoa hồng. Bốn thứ đó sai một cái là tiền vào nhầm ví.
Bài test đó dán nguyên văn một payload thật vào, cho chạy hết chuỗi xử lý, rồi kiểm từng con số một. Mã sàn phải suy ra đúng từ trường này, mã tracking phải tách đúng ra từ chuỗi kia, tiền phải ra đúng số đó.
Nó là E2E theo đúng nghĩa đi hết một luồng, mà lại chạy trong vài mili giây và nằm luôn trong bộ test thường ngày.
Đừng hỏi test này thuộc tầng nào. Hỏi là nó đi qua bao nhiêu chỗ có thể hiểu lầm nhau.
Cái trục đáng quan tâm không phải nhãn unit hay integration hay E2E, mà là đi tới đâu thì còn là thật. Càng nhiều mắt xích thật thì càng bắt được nhiều bug ghép nối, đổi lại là chạy chậm và dựng mệt. Chọn điểm dừng ở đâu là tuỳ chỗ đó có bao nhiêu tiền chảy qua.
6. Đợt soi trang quản trị, và ba mươi lăm cái bug không có cái nào là lỗi thuật toán
Đây là đợt nặng nhất, hồi đầu tháng bảy.
Trang quản trị của mình phình ra nhanh, mỗi lần thêm tính năng là thêm vài màn, và gần như không ai ngồi đi lại toàn bộ bao giờ. Mình quyết định làm một đợt cho tử tế, vừa soi hợp đồng dữ liệu giữa giao diện và API, vừa mở trình duyệt lên bấm thật từng màn một.
Kết quả là khoảng ba mươi lăm cái bug, gom lại thành một nhánh riêng để sửa dần.
Điều làm mình khựng lại không phải con số, mà là không có cái nào trong đó là lỗi tính toán cả. Toàn là hai bên hiểu khác nhau.
Ô tìm kiếm trên sáu màn danh sách là đồ trang trí, gõ vào không có gì xảy ra, vì phía API chưa bao giờ nhận tham số tìm kiếm. Màn chi tiết người dùng thiếu một loạt thông tin, chỉ vì backend trả tên trường theo kiểu gạch dưới còn giao diện đọc theo kiểu viết hoa chữ cái. Mấy màn báo cáo vỡ hợp đồng sau một đợt đổi tên khái niệm trong hệ thống, giao diện vẫn đọc theo tên cũ. Cái thứ tự hiển thị thì lấy giá trị lớn nhất cộng một, nhưng cộng nhầm kiểu chuỗi, nên dãy số chạy thành không, một, mười một, một trăm mười một.
Mỗi lỗi này soi riêng từng file đều không thấy gì sai. Đầu bên kia làm đúng phần của nó, đầu bên này cũng đúng phần của nó. Chỉ có điều hai bên chưa bao giờ ngồi lại với nhau, và không có ai đóng vai người dùng để bấm thử.
7. Không phải finding nào của E2E cũng nên sửa ngay
Cùng đợt đó còn ra khoảng chục cái nữa mà mình cố tình không đụng tới, ghi hết vào một mục theo dõi riêng.
Ví dụ chuyện múi giờ. Giao diện gửi lên một cái mốc thời gian trần trụi không kèm múi giờ, backend nhận rồi diễn giải theo múi giờ của máy chủ, mà máy chủ chạy giờ quốc tế. Thành ra cái lịch admin đặt bị lệch bảy tiếng so với ý họ, và lúc mở ra sửa lại thì hiện ra một con số khác nữa. Sửa được, nhưng phải chốt quy ước cho cả hệ thống trước, chứ vá một màn thì màn khác lại lệch kiểu khác.
Hay như chỗ nhận ngày tháng của báo cáo, gõ vào một cái ngày vô lý kiểu tháng mười ba ngày bốn mươi lăm thì nó im lặng tự trôi sang tháng hai năm sau, không báo lỗi gì.
Mình để lại vì hai lý do. Một là chúng cần một quyết định về thiết kế chứ không phải một dòng code. Hai là khi E2E ném ra ba bốn chục cái cùng lúc, mà mình hăng máu sửa hết trong một nhánh, thì cái nhánh đó biến thành một đợt refactor mù, review không nổi và không ai dám merge.
E2E là công cụ để nhìn thấy, không phải cái lệnh bắt phải sửa hết ngay.
8. Có loại bug chỉ chịu lộ ra khi chạy đúng cách production chạy
Cái này mình nhớ nhất, vì suýt nữa thì nó lên thẳng production.
Dịch vụ API quản trị chạy local thì ngon lành, test xanh, kiểu gì cũng ổn. Đóng gói lên thì pod chết trong vòng lặp khởi động lại liên tục.
Nguyên nhân là đường dẫn rút gọn trong import. Trình biên dịch không viết lại mấy đường dẫn đó khi xuất ra bản build, nên bản build vẫn còn nguyên đường dẫn rút gọn, và lúc chạy thì nó lần ngược về thư mục mã nguồn. Kết quả là một service bị nạp hai lần từ hai đường khác nhau, hệ thống tiêm phụ thuộc coi đó là hai thứ khác nhau, và nó ngã ngay lúc khởi động.
Chạy ở chế độ phát triển thì đọc thẳng mã nguồn nên không bao giờ lộ. Muốn thấy nó thì phải khởi động đúng bản đã đóng gói, đúng cách máy chủ thật khởi động.
Từ đó mình thêm một bước rất rẻ trước khi phát hành, là chạy thử bản build ở local và xem nó có in ra dòng khởi động thành công không. Một phút, và nó bắt được đúng cái loại bug mà toàn bộ bộ test cộng lại không bắt nổi.
9. Sau khi lên production thì vẫn phải đi thử một vòng
Cái cuối cùng, và mình nghĩ là quan trọng nhất với sản phẩm có tiền chạy qua.
Trong tài liệu phát hành của mình có một mục riêng, gọi là phép thử xuyên hệ. Nó chỉ có đúng hai ba việc. Một đơn hàng thật đi từ tiện ích trình duyệt, qua dịch vụ thu thập, qua dịch vụ trung gian, bắn về backend, và cuối cùng là kiểm xem ví của người dùng có nhảy đúng số tiền không. Một đơn nữa đi bằng đường postback của mạng affiliate, kiểm cả phần chia hoa hồng.
Nó không phải bộ test tự động, không chạy trong CI, và mình cũng không định làm nó thành bộ test tự động. Nó là một cái danh sách để người ngồi làm sau mỗi lần phát hành.
Lý do là ở đây có những thứ chỉ tồn tại trên production. Biến môi trường thật, khoá bí mật thật, đối tác thật, dữ liệu cũ có đủ hình thù kỳ dị mà không ai nghĩ ra được lúc ngồi viết test. Bộ test xanh chỉ chứng minh mã nguồn đúng, nó không chứng minh cái hệ thống đang chạy ngoài kia đúng.
Và trong một sản phẩm hoàn tiền thì câu hỏi duy nhất đáng hỏi sau mỗi lần deploy là tiền có về đúng ví không. Câu đó không có test nào trả lời thay được, phải tự đi mua một đơn rồi ngồi xem.
Kết bài
Nếu tóm lại thành một câu thì thế này. Mấy năm đầu mình nghĩ viết nhiều test là cẩn thận. Bây giờ mình nghĩ cẩn thận là biết bộ test của mình đang không nhìn thấy cái gì.
Bộ test của mình không nhìn thấy chữ bị cắt trên màn hình. Không nhìn thấy hai đầu gọi nhau bằng hai kiểu tên trường khác nhau. Không nhìn thấy cái ô tìm kiếm không làm gì cả. Không nhìn thấy bản build khác bản mã nguồn. Không cái nào trong số đó là lỗi trong một hàm, nên không unit test nào đỏ được.
Nên nếu bạn đang định bắt đầu với E2E, mình gợi ý ba việc rẻ tiền này trước, đừng vội dựng cả một bộ khung.
Một là lấy đúng cái luồng quan trọng nhất của sản phẩm, thứ mà hỏng là mất tiền hoặc mất người dùng, viết cho nó một bài chạy trên máy thật và chụp lại từng màn. Chỉ một luồng thôi. Với mình là ba màn onboarding và một đơn hàng đi tới ví.
Hai là chọn một luồng dữ liệu đi qua nhiều lớp, dán nguyên payload thật vào rồi kiểm mấy con số ở đầu ra. Loại này gần như không tốn gì mà bắt được kha khá.
Ba là trước khi phát hành, chạy thử đúng bản đã đóng gói ở local một lần. Chỉ cần xem nó khởi động được không.
Còn nếu sản phẩm của bạn có một trang quản trị mà lâu rồi không ai đi lại từ đầu, thì thử dành một buổi mở nó lên bấm hết từng màn xem. Mình đoán là bạn sẽ tìm ra vài cái ô tìm kiếm chỉ để cho đẹp, giống mình.
Và thôi đến đây thôi, chúc các bạn có những có những dòng code thật nà chất nượng! Xin chào và hẹn gặp lại các bạn ở những bài viết sau