Mở đầu — vì sao bài này quan trọng
Push notification (thông báo đẩy) là kênh giao tiếp trực tiếp nhất mà một ứng dụng có với người dùng khi họ không mở app. Với sản phẩm thương mại điện tử, ví điện tử hay ứng dụng gọi xe ở Việt Nam, một cú push đúng lúc — "Đơn hàng của bạn đang trên đường giao", "Bạn vừa nhận 50.000đ hoàn tiền" — chính là thứ kéo người dùng quay lại app và tạo ra doanh thu. Ngược lại, một cú push sai (spam lúc 2 giờ sáng, deep link dẫn tới màn hình trắng, hay thông báo tiếng Việt bị vỡ dấu) có thể khiến người dùng tắt notification vĩnh viễn — và đó là một con đường một chiều: rất khó lấy lại quyền được gửi thông báo cho họ.
Điều khiến push notification đặc biệt khó test là nó là một luồng bất đồng bộ, đa thành phần mà bạn không kiểm soát toàn bộ. Message của bạn phải đi qua backend → dịch vụ push của Apple hoặc Google → mạng viễn thông → hệ điều hành của thiết bị → cuối cùng mới tới app. Mỗi mắt xích đều có thể im lặng nuốt mất thông báo mà không báo lỗi. Vì thế, QA mobile giỏi không chỉ kiểm tra "thông báo có hiện không" mà phải hiểu toàn bộ chuỗi này, biết chỗ nào dễ đứt, và biết cách chứng minh vấn đề nằm ở đâu. Đó chính là nội dung bài này.
Khái niệm cốt lõi
Hai nhà cung cấp dịch vụ push chính
Có hai "bưu điện" trung tâm mà mọi push notification phải đi qua:
- APNs (Apple Push Notification service) — dịch vụ độc quyền của Apple cho mọi thiết bị iOS, iPadOS, watchOS, macOS. Không có cách nào gửi push tới iPhone mà không đi qua APNs.
- FCM (Firebase Cloud Messaging) — dịch vụ của Google, mặc định cho Android. Điều nhiều bạn mới không biết: FCM cũng có thể gửi tới iOS, nhưng khi đó FCM chỉ đóng vai trò lớp trung gian rồi chuyển tiếp message qua APNs. Nghĩa là trên iOS, APNs luôn là mắt xích cuối cùng.
Device token — trái tim của mọi push
Để gửi push tới đúng một máy cụ thể, backend cần một device token (APNs) hoặc registration token (FCM). Token này do hệ điều hành cấp cho app khi người dùng đồng ý nhận thông báo. Vòng đời của nó là nguồn gốc của phần lớn bug:
- Token thay đổi khi người dùng cài lại app, khôi phục máy từ backup, hoặc đôi khi sau khi cập nhật OS.
- Token hết hạn / vô hiệu khi app bị gỡ. Nếu backend vẫn gửi tới token cũ, APNs trả về lỗi
Unregistered/BadDeviceToken; FCM trả vềNotRegistered. Backend phải dọn token chết, nếu không tỉ lệ gửi thành công sẽ tụt dần theo thời gian. - Token của môi trường sandbox và production trên iOS là khác nhau. Đây là cái bẫy kinh điển: build development chỉ nhận được push từ endpoint sandbox của APNs, build production (TestFlight/App Store) chỉ nhận từ endpoint production. Gửi nhầm endpoint = im lặng, không có thông báo, không có lỗi rõ ràng.
Ba loại payload cần phân biệt
- Alert / Notification message — có tiêu đề, nội dung, âm thanh, badge. Khi app đang ở background, hệ điều hành tự hiển thị, code app không chạy.
- Data-only / Silent push — không hiển thị gì, chỉ chở dữ liệu để app xử lý ngầm (ví dụ đồng bộ giỏ hàng). Trên iOS cần cờ
content-available: 1; loại này bị OS bóp rất mạnh về tần suất. - Notification + data — vừa hiển thị vừa mang payload (thường chứa deep link). Đây là loại hay dùng nhất và cũng hay lỗi nhất, vì hành vi khác nhau tùy app đang foreground, background hay killed.
Quyền thông báo và các "cửa chặn"
Kể từ iOS, app bắt buộc xin quyền authorization mới được hiện thông báo. Android 13 (API 33) trở đi cũng yêu cầu quyền POST_NOTIFICATIONS runtime. Ngoài ra còn nhiều lớp chặn ngầm mà QA hay bỏ sót: Focus/Do Not Disturb, notification channel bị người dùng tắt (Android), chế độ tiết kiệm pin khiến máy Android trì hoãn/nuốt push (đặc biệt các máy Trung Quốc như Xiaomi, OPPO, vivo phổ biến ở Việt Nam có cơ chế "kill app nền" rất hung hãn).
Tình huống thực tế
Tình huống 1 — Ví điện tử và cú push sandbox/production
Một ví điện tử giả định tên PayGo ở TP.HCM chuẩn bị ra mắt tính năng "thông báo giao dịch tức thời". QA test trên build development suốt hai tuần: mọi cú chuyển tiền đều bắn notification trong vòng 2 giây, hoàn hảo. Nhưng ngay ngày phát hành qua TestFlight, khiếu nại đổ về: người dùng không nhận được thông báo giao dịch nào.
Điều tra ra: backend đang gọi một endpoint APNs cố định là api.sandbox.push.apple.com. Trên build development điều này đúng, nên QA không bao giờ thấy vấn đề. Nhưng TestFlight là môi trường production, token của nó chỉ được endpoint api.push.apple.com phục vụ. Kết quả là APNs im lặng từ chối — không lỗi hiển thị cho người dùng, chỉ có mã BadDeviceToken chôn trong log server mà không ai đọc.
Bài học: với iOS, bắt buộc có ít nhất một vòng test trên build phân phối thật (TestFlight), không được kết luận "push chạy tốt" chỉ dựa trên build cắm cáp Xcode. Và QA phải yêu cầu dev cho xem phản hồi từ APNs/FCM, coi mã lỗi là một phần của test chứ không chỉ nhìn màn hình điện thoại.
Tình huống 2 — Ứng dụng giao đồ ăn và deep link chết
Một app giao đồ ăn kiểu ShopeeFood/GrabFood gửi push "Tài xế đã nhận đơn — bấm để theo dõi". Payload chứa deep link app://order/track/{orderId}. QA test kịch bản chuẩn: mở app, ở màn hình chính, nhận push, bấm vào → nhảy đúng màn hình tracking. Pass. Lên production, một loạt review 1 sao: "bấm thông báo mở app xong màn hình trắng".
Nguyên nhân: QA chỉ test khi app đang chạy nền. Khi app đã bị kill hoàn toàn (người dùng vuốt tắt, hoặc bị OS thu hồi sau vài giờ), việc bấm push sẽ cold start toàn bộ app. Lúc này màn hình tracking cố đọc orderId nhưng session/route chưa kịp khởi tạo, gây crash sang màn hình trắng. Ba trạng thái app — foreground, background, killed — cho ra ba luồng code khác nhau khi xử lý cú bấm notification, và trạng thái killed là nơi bug ẩn nấp nhiều nhất.
Bài học: mỗi test case deep link qua push phải chạy đủ ba trạng thái. Riêng "killed" cần vuốt tắt app khỏi khay đa nhiệm trước khi bắn push. Đây gần như luôn là test case bị bỏ quên.
Tình huống 3 — Tiếng Việt vỡ dấu và giới hạn payload
Một sàn TMĐT gửi push khuyến mãi tiếng Việt: "Giảm 50% — Mua ngay kẻo lỡ!". Trên máy test của QA (iPhone) hiển thị đẹp. Nhưng nhiều người dùng Android báo thông báo hiện thành "Gi?m 50% — Mua ngay k?o l?" hoặc ký tự lạ. Truy ra: một service phía backend nối chuỗi bằng encoding không phải UTF-8, khiến dấu tiếng Việt (ă, ơ, ẻ...) bị hỏng khi đóng gói JSON payload.
Một biến thể khác cũng hay gặp: nội dung khuyến mãi quá dài. APNs giới hạn payload khoảng 4KB (silent push còn thấp hơn); vượt quá thì cú push bị từ chối toàn bộ — không phải cắt ngắn mà là mất luôn. Marketing nhét cả emoji, tên sản phẩm dài, URL ảnh vào payload là dễ chạm trần.
Bài học: với sản phẩm Việt Nam, luôn có test case nội dung tiếng Việt có dấu đầy đủ trên cả iOS lẫn nhiều máy Android khác hãng. Và luôn test một payload "dài tối đa" để bắt lỗi vượt giới hạn kích thước.
Hướng dẫn từng bước
Đây là quy trình test một tính năng push notification từ đầu tới cuối:
- Xác nhận cấu hình nền tảng. Kiểm tra app có
aps-environmententitlement (iOS) và Firebase config (GoogleService-Info.plist/google-services.json) đúng môi trường. Hỏi dev: build này gọi endpoint sandbox hay production?
- Test luồng xin quyền. Cài mới app → xác nhận dialog xin quyền hiện đúng lúc, đúng nội dung. Test cả nhánh Cho phép và Từ chối. Với nhánh từ chối, app phải xử lý mượt (dẫn tới Cài đặt nếu người dùng đổi ý), không được crash hay gửi push vô ích.
- Lấy và kiểm tra token. Xác nhận app đăng ký được device token và gửi lên backend thành công. Ghi lại token để dùng gửi thử thủ công.
- Gửi push thử bằng công cụ. Trước khi có backend hoàn chỉnh, dùng công cụ gửi trực tiếp: Firebase Console (tab Messaging), hoặc script gửi APNs. Điều này tách bạch lỗi "app không nhận" khỏi lỗi "backend không gửi".
- Chạy ma trận trạng thái × loại payload. Với mỗi loại (alert, data, notification+deeplink), test đủ ba trạng thái app: foreground, background, killed. Ghi kết quả vào bảng.
- Test deep link tới đích. Bấm vào notification → xác nhận điều hướng đúng màn hình, đúng dữ liệu, ở cả ba trạng thái. Đặc biệt kiểm cold start (killed).
- Test hiển thị nội dung. Tiếng Việt có dấu, emoji, chuỗi dài, badge count, âm thanh, nhóm thông báo (grouping). Kiểm trên nhiều máy Android khác hãng.
- Test các cửa chặn. Bật Do Not Disturb / Focus, tắt notification channel, bật tiết kiệm pin, đặc biệt trên máy Xiaomi/OPPO. Xác nhận hành vi hợp lý và tài liệu hóa giới hạn của OS.
- Test dọn token chết. Gỡ app, sau đó backend gửi push → xác nhận backend nhận mã
NotRegistered/Unregisteredvà xóa token, không gửi lặp lại.
Lỗi thường gặp & mẹo
- Kết luận sai từ build development. Như tình huống PayGo: luôn có ít nhất một vòng test trên TestFlight/Internal track thật.
- Bỏ quên trạng thái killed. Đây là lỗi số một. Tạo checklist bắt buộc ba trạng thái cho mọi test case push.
- Nhầm foreground. Khi app đang mở, OS không tự hiện banner — đừng vội báo "bug không có thông báo" mà chưa kiểm app có đang foreground không.
- Chỉ test một dòng máy. Android phân mảnh nặng; máy Trung Quốc phổ biến ở Việt Nam nuốt push nền rất mạnh. Cần ít nhất một Samsung, một Xiaomi/OPPO trong bộ máy test.
- Không đọc phản hồi từ APNs/FCM. Màn hình điện thoại chỉ là một nửa sự thật. Yêu cầu quyền xem log gửi để đọc mã lỗi (
BadDeviceToken,NotRegistered, payload quá lớn...). - Quên silent push bị bóp. iOS giới hạn tần suất silent push và không đảm bảo giao ngay; đừng test kỳ vọng nó tức thời như alert.
- Mẹo: dựng sẵn một "bảng ma trận push" (Google Sheet) làm template tái sử dụng cho mọi tính năng — hàng là loại payload, cột là trạng thái app × nền tảng. Điền pass/fail cực nhanh và không sót ô nào.
- Mẹo: dùng công cụ gửi thủ công (Firebase Console, script APNs) để cô lập lỗi giữa app và backend ngay từ đầu — tiết kiệm hàng giờ đổ lỗi qua lại.
Bài tập thực hành
- Dựng ma trận test. Lấy một app có push (hoặc một app bất kỳ trên máy bạn) và tạo bảng ma trận: 3 loại payload × 3 trạng thái app × 2 nền tảng. Viết mô tả kết quả mong đợi cho từng ô.
- Gửi push thủ công. Tạo một project Firebase, tích hợp FCM vào một app demo, lấy registration token, rồi gửi một notification từ Firebase Console tới chính máy bạn. Sau đó thử gửi một payload chứa deep link.
- Săn bug trạng thái killed. Với app demo có deep link qua push, vuốt tắt app hoàn toàn rồi bấm notification. Ghi lại xem cold start có dẫn đúng màn hình không, có màn hình trắng/crash không.
- Test tiếng Việt và giới hạn. Gửi một push nội dung tiếng Việt đầy đủ dấu + emoji trên cả iOS và một máy Android khác hãng. Sau đó cố tình nhồi payload thật dài để chạm giới hạn ~4KB và quan sát điều gì xảy ra.
- Viết báo cáo bug mẫu. Giả sử bạn tìm được lỗi "push không tới trên Xiaomi khi bật tiết kiệm pin". Viết một bug report đầy đủ: bước tái hiện, môi trường (máy, OS, trạng thái app), kết quả mong đợi vs thực tế, và mã lỗi FCM nếu có.
Tóm tắt
Push notification là một luồng bất đồng bộ đi qua nhiều mắt xích — backend, APNs hoặc FCM, mạng, OS, app — và mỗi mắt xích đều có thể im lặng làm mất thông báo. Là QA mobile, bạn cần nắm ba trụ cột: hai nhà cung cấp (APNs cho iOS, FCM cho Android và cũng chuyển tiếp qua APNs cho iOS), vòng đời device token (thay đổi, hết hạn, khác nhau giữa sandbox/production), và ma trận trạng thái app × loại payload làm xương sống test case. Ba cạm bẫy phổ biến nhất: nhầm sandbox/production, bỏ quên trạng thái killed khi test deep link, và tiếng Việt vỡ dấu do encoding. Luôn test trên build phân phối thật, luôn phủ đủ ba trạng thái app, luôn kiểm tra trên nhiều dòng máy Android, và đừng bao giờ chỉ tin vào màn hình điện thoại — hãy đọc cả mã phản hồi từ APNs/FCM. Làm chủ được những điều này, bạn đã bảo vệ được kênh giao tiếp giá trị nhất mà sản phẩm có với người dùng.