Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 28 — Mobile-Specific Bug Patterns

Đã đọc 0/14 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Nếu bạn từng đọc một bug report kiểu "app bị lỗi", bạn sẽ hiểu nỗi ám ảnh của một Mobile QA. Nhưng khi làm nghề đủ lâu, bạn sẽ nhận ra một sự thật thú vị: phần lớn bug trên mobile không phải là ngẫu nhiên. Chúng lặp đi lặp lại theo những khuôn mẫu (pattern) giống nhau đến kỳ lạ, dù bạn test app ngân hàng, app đặt đồ ăn hay app đọc báo. Cùng một loại lỗi — mất state khi xoay màn hình, token hết hạn khi app quay lại từ nền, keyboard che mất nút submit — xuất hiện hết dự án này đến dự án khác.

Đây chính là tin tốt cho bạn. Vì bug có khuôn mẫu, nên bạn có thể chủ động săn chúng thay vì bị động chờ chúng nổ ra ngoài production. Một QA giỏi không phải người test nhanh nhất, mà là người biết trước "chỗ này khả năng cao sẽ vỡ" và đi thẳng tới đó. Trong khi các bài trước (Bài 4, Bài 5) cho bạn nền tảng về khác biệt nền tảng và thiết bị, còn các bài sau đi sâu vào từng loại test chuyên biệt (accessibility, performance, network...), thì bài này đóng vai trò như một checklist tư duy — một danh sách các khuôn mẫu lỗi đặc thù của mobile mà bạn nên luôn mang theo trong đầu mỗi khi nhận một tính năng mới để test.

Chúng ta sẽ đi qua top các pattern nguy hiểm nhất, hiểu tại sao chúng xảy ra (căn nguyên kỹ thuật), và quan trọng nhất là cách săn chúng một cách có hệ thống.

Khái niệm cốt lõi

Mobile-specific bug pattern là những lỗi phát sinh chính vì bản chất của môi trường mobile — nơi app không bao giờ chạy trong một môi trường ổn định như trên desktop. Điện thoại bị xoay, bị khóa, bị chuyển sang app khác, mất mạng giữa chừng, hết pin, bị hệ điều hành "giết" tiến trình để nhường RAM cho app khác. Mỗi biến động đó là một cơ hội để state bị hỏng.

Hãy nắm vững vòng đời (lifecycle) là gốc rễ của phần lớn pattern này. Khác với web, app mobile liên tục chuyển giữa các trạng thái: đang chạy (foreground/active), tạm ẩn (inactive), chạy nền (background), bị treo (suspended), và bị hệ điều hành thu hồi (terminated). Bug xuất hiện tại các điểm chuyển tiếp giữa những trạng thái này.

Nhóm 1 — Bug do biến động UI và cấu hình

Đây là nhóm dễ tái hiện nhất và cũng bị bỏ sót nhiều nhất.

  • Screen rotation (xoay màn hình): Trên Android, khi xoay màn hình, mặc định hệ thống hủy và tạo lại Activity. Nếu lập trình viên không lưu state đúng cách (onSaveInstanceState), toàn bộ dữ liệu người dùng đang nhập sẽ biến mất. Bạn điền form đăng ký dài 10 trường, xoay ngang để nhìn cho rõ, và tất cả trở về trắng tinh.
  • Font size / display size lớn: Nhiều người dùng lớn tuổi bật cỡ chữ hệ thống lên mức tối đa. Layout bị vỡ, text bị cắt (truncate), nút bị đè lên nhau, hoặc nút submit bị đẩy ra khỏi màn hình.
  • Keyboard che UI: Bàn phím bật lên che mất trường nhập liệu hoặc nút "Xác nhận" ở cuối form. Người dùng không cuộn được tới nút đó.
  • Split-screen / multi-window: Trên tablet và Android hiện đại, app chạy trong nửa màn hình. Layout responsive không xử lý được kích thước bất thường này.

Nhóm 2 — Bug do lifecycle (background/foreground)

Đây là nhóm nguy hiểm nhất vì khó tái hiện và thường liên quan tới bảo mật.

  • Token hết hạn khi quay lại: Người dùng mở app, đăng nhập, rồi chuyển sang app khác 30 phút. Khi quay lại, access token đã hết hạn. App tốt sẽ âm thầm refresh token; app kém sẽ crash, treo màn hình loading vô tận, hoặc hiển thị dữ liệu rỗng mà không báo lỗi.
  • State không được khôi phục sau khi bị OS terminate: Android/iOS có thể "giết" app đang chạy nền để giải phóng RAM. Khi người dùng mở lại, họ mong đợi app quay về đúng màn hình đang xem — nhưng nhiều app "quên sạch" và văng về màn hình chủ.
  • Timer, video, WebSocket không tạm dừng khi vào nền: Video vẫn phát âm thanh khi app đã ẩn, hoặc countdown timer chạy sai vì không tính thời gian ở nền.

Nhóm 3 — Bug do gián đoạn ngoại cảnh (interruption)

  • Cuộc gọi đến / báo thức: Đang quay video trong app, có cuộc gọi tới — app xử lý thế nào khi được trả lại?
  • Thông báo kéo xuống, đổi app rồi quay lại: State của form thanh toán còn nguyên không?
  • Mất mạng giữa một giao dịch: Đã trừ tiền nhưng chưa nhận phản hồi — đây là pattern "double-charge" kinh điển.

Nhóm 4 — Bug do dữ liệu và định danh thiết bị

  • Deep link mở sai màn hình khi app đang ở trạng thái khác nhau (đã mở / chưa mở / bị terminate).
  • Race condition khi tap nhanh 2 lần vào nút "Đặt hàng" — tạo 2 đơn trùng.
  • Xử lý sai khi permission bị thu hồi giữa chừng từ phần Cài đặt hệ thống.

Tình huống thực tế

Ví dụ 1 — Ví điện tử và cú "double charge" khi mất mạng (bối cảnh Việt Nam)

Một team QA của một ví điện tử Việt Nam (giả định gọi là "PayViet") gặp báo cáo từ CSKH: khách hàng ở khu vực sóng yếu tại các tỉnh phàn nàn bị trừ tiền hai lần khi nạp điện thoại. Điều tra cho thấy pattern kinh điển: người dùng bấm "Nạp 50.000đ", request được gửi đi, nhưng do 3G chập chờn, phản hồi không về kịp trong 15 giây. Nút vẫn ở trạng thái "bấm được", người dùng sốt ruột bấm lại. Backend nhận hai request giống hệt và xử lý cả hai.

Đội QA vốn chỉ test trên Wi-Fi văn phòng mạnh nên không bao giờ tái hiện được. Sau khi họ thêm bước giả lập mạng chậm (network throttling) và tap nhanh nhiều lần vào quy trình test cho mọi nút giao dịch tiền, họ phát hiện lỗi thiếu idempotency key — mỗi request nạp tiền cần một mã định danh duy nhất để backend nhận diện và từ chối request trùng.

Bài học: Bug pattern "double-submit trong điều kiện mạng kém" phải được test có chủ đích với thiết bị bị bóp băng thông, không phải chỉ trên Wi-Fi lý tưởng. Với mọi hành động liên quan tới tiền, luôn hỏi: "Điều gì xảy ra nếu người dùng bấm hai lần?".

Ví dụ 2 — App đọc báo và cú xoay màn hình xóa sạch bình luận (bối cảnh Đông Nam Á)

Một app tin tức phổ biến ở khu vực có tính năng viết bình luận dài. QA nhận bug từ cộng đồng: người dùng viết bình luận cả đoạn văn, tiện tay xoay ngang điện thoại để gõ thoải mái hơn, thì toàn bộ nội dung biến mất.

Nguyên nhân đúng như sách giáo khoa: trên Android, xoay màn hình khiến Activity bị tạo lại, và trường EditText của bình luận không được đưa vào onSaveInstanceState. Điều tệ hơn: lỗi này xảy ra với khoảng 8% người dùng có bật tính năng tự động xoay, một tỷ lệ đủ lớn để tạo làn sóng review 1 sao trên store.

Sau sự cố, đội QA đưa "xoay màn hình ở mọi màn hình có nhập liệu" thành một mục bắt buộc trong test suite hồi quy (regression). Họ còn dùng một mẹo hay: trong Developer Options của Android, bật tùy chọn "Don't keep activities" (Không giữ activity) để hệ thống hủy activity ngay khi vào nền — cách nhanh nhất để phơi bày mọi lỗi mất state mà không cần chờ OS terminate ngẫu nhiên.

Bài học: Mất state khi thay đổi cấu hình là pattern số một cần săn. Công cụ để săn nó (Don't keep activities) đã có sẵn ngay trong máy, hãy dùng nó cho mọi màn hình có form.

Ví dụ 3 — App gọi xe và token chết sau giờ nghỉ trưa

Một startup gọi xe có tính năng cho tài xế bật app cả ngày. QA phát hiện: nhiều tài xế mở app buổi sáng, để app chạy nền suốt giờ nghỉ trưa (khoảng 90 phút không thao tác), rồi khi mở lại để nhận cuốc, màn hình bản đồ đứng im, không có cuốc nào hiện ra, cũng không có thông báo lỗi. Tài xế tưởng vắng khách, thực ra app đã "chết ngầm".

Điều tra cho thấy access token có hạn 60 phút. Khi app quay lại foreground sau 90 phút, kết nối WebSocket nhận cuốc bị đứng vì token hết hạn, nhưng app không có cơ chế phát hiện và refresh token khi resume — nó chỉ refresh khi khởi động lạnh (cold start). Vì đa số tester luôn tắt hẳn app rồi mở lại (cold start), họ không bao giờ chạm tới đường đi "warm resume sau thời gian dài".

Fix là thêm logic kiểm tra và refresh token tại sự kiện onResume/applicationDidBecomeActive. QA thì bổ sung một kịch bản chuẩn: để app ở nền vượt quá thời gian sống của token rồi mới quay lại.

Bài học: "Background → foreground với token hết hạn" là pattern bảo mật/độ tin cậy hàng đầu. Đừng chỉ test cold start; hãy test warm resume, và đặc biệt là warm resume sau khi token đã hết hạn.

Hướng dẫn từng bước

Đây là quy trình săn mobile bug pattern một cách có hệ thống cho bất kỳ tính năng nào bạn nhận được.

  • Lập bản đồ các màn hình có state. Liệt kê mọi màn hình có dữ liệu người dùng đang tạm giữ: form nhập liệu, giỏ hàng, bước thanh toán đang dở, bộ lọc đã chọn. Đây chính là các "nạn nhân" tiềm năng của bug lifecycle.
  • Chạy checklist xoay & cấu hình. Ở mỗi màn hình có state: nhập dữ liệu dở dang → xoay ngang → xoay dọc lại. Dữ liệu còn không? Lặp lại với cỡ chữ hệ thống lớn nhất và với chế độ split-screen.
  • Chạy checklist lifecycle. Bật Developer Options → Don't keep activities (Android). Với mỗi màn hình: nhập dữ liệu → nhấn Home để vào nền → mở app khác → quay lại. Sau đó test warm resume sau thời gian dài (dùng app hẹn giờ hoặc chỉnh đồng hồ hệ thống để vượt qua thời gian sống của token).
  • Chạy checklist gián đoạn. Trong lúc đang ở giữa một luồng quan trọng (đang thanh toán, đang quay video): giả lập cuộc gọi đến, kéo notification center, khóa màn hình rồi mở lại. Kiểm tra state có nguyên vẹn.
  • Chạy checklist mạng & tap kép. Bóp băng thông xuống mức "3G kém" hoặc bật chế độ máy bay giữa chừng một request. Với mọi nút gây tác dụng phụ (đặt hàng, trả tiền), tap nhanh 2–3 lần liên tiếp.
  • Chạy checklist permission & deep link. Vào Cài đặt hệ thống thu hồi một quyền (vị trí, camera) khi app đang chạy, rồi quay lại app. Mở app qua deep link ở cả ba trạng thái: chưa mở, đang chạy nền, đã bị terminate.
  • Ghi lại và phân loại theo pattern. Khi tìm ra bug, gắn nhãn nó theo pattern (ví dụ: "lifecycle - mất state", "network - double submit"). Việc này giúp cả team nhận ra pattern nào hay lặp lại để đưa vào Definition of Done.

Lỗi thường gặp & mẹo

Lỗi 1 — Chỉ test trên Wi-Fi văn phòng mạnh. Đây là cái bẫy lớn nhất. Người dùng thật ở trong thang máy, dưới hầm gửi xe, trên xe bus vùng sóng yếu. Mẹo: luôn có một session test với network throttling và một session bật/tắt chế độ máy bay giữa luồng.

Lỗi 2 — Luôn cold start. Tester quen tay tắt hẳn app rồi mở lại, bỏ lỡ toàn bộ nhóm bug warm resume. Mẹo: có ý thức phân biệt cold start (mở từ app đã tắt hẳn) và warm resume (đưa từ nền lên), test cả hai.

Lỗi 3 — Không tận dụng Developer Options. "Don't keep activities" là vũ khí mạnh nhất để phơi bày lỗi mất state mà nhiều QA không biết. Mẹo: bật nó lên trong suốt cả chu kỳ test lifecycle.

Lỗi 4 — Quên rằng iOS và Android xử lý lifecycle khác nhau. Android hủy/tạo lại Activity khi xoay; iOS thì không tạo lại view controller nhưng lại có cơ chế snapshot màn hình khi vào nền (dễ lộ dữ liệu nhạy cảm trong app switcher). Mẹo: với app tài chính, kiểm tra ảnh xem trước trong app switcher có che dữ liệu nhạy cảm không.

Lỗi 5 — Bug tái hiện không ổn định bị bỏ qua. Race condition và lỗi lifecycle thường "lúc có lúc không". Đừng đóng bug là "không tái hiện được". Mẹo: ghi lại chính xác điều kiện (thiết bị, tốc độ mạng, thời gian ở nền) và tăng số lần lặp để bắt bug xác suất.

Mẹo vàng: Xây một "smoke test lifecycle" chuẩn hóa, áp dụng cho mọi màn hình mới. Chỉ mất 5 phút mỗi màn hình nhưng chặn được cả một lớp bug đắt đỏ.

Bài tập thực hành

  • Săn bug xoay màn hình: Chọn một app bất kỳ trên máy bạn có form nhập liệu (ví dụ app ghi chú, app mua sắm). Nhập dữ liệu dở dang, xoay màn hình. Ghi lại app nào giữ được state, app nào mất. Viết một bug report chuẩn cho một trường hợp mất state.
  • Kịch bản warm resume với token: Với một app cần đăng nhập, để app ở nền hơn thời gian bạn ước lượng token sống (thử 30–60 phút), rồi quay lại và thao tác. Quan sát app xử lý ra sao. Viết ra 3 câu hỏi test bạn sẽ hỏi lập trình viên về cơ chế refresh token.
  • Thiết kế checklist: Dựa trên bài học, tự soạn một bảng checklist "Mobile Bug Pattern" gồm ít nhất 12 mục, chia theo 4 nhóm (UI/cấu hình, lifecycle, gián đoạn, mạng/dữ liệu). Đây sẽ là công cụ bạn mang theo trong mọi dự án.
  • Tap kép: Tìm một app có nút gây tác dụng phụ (gửi tin, thêm giỏ hàng). Bóp mạng chậm, tap nhanh 2 lần. Kết quả có bị nhân đôi không? Ghi lại phát hiện.

Tóm tắt

Bug trên mobile không ngẫu nhiên — chúng lặp theo khuôn mẫu, và đó là lợi thế của bạn. Bốn nhóm pattern cần luôn ghi nhớ: biến động UI/cấu hình (xoay màn hình, cỡ chữ lớn, keyboard che UI), lifecycle (mất state khi bị terminate, token chết khi warm resume), gián đoạn ngoại cảnh (cuộc gọi, chuyển app, mất mạng giữa giao dịch), và dữ liệu/định danh (double submit, deep link sai trạng thái, permission bị thu hồi).

Ba bài học then chốt từ thực tế: đừng chỉ test trên Wi-Fi mạnh (hãy bóp mạng), đừng chỉ cold start (hãy test warm resume, đặc biệt sau khi token hết hạn), và hãy tận dụng "Don't keep activities" để phơi bày lỗi mất state. Cuối cùng, biến quy trình săn bug thành một checklist lifecycle chuẩn hóa mà bạn áp dụng cho mọi màn hình mới — 5 phút bỏ ra để chặn cả một lớp bug đắt đỏ trước khi chúng lên tay người dùng thật.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi