Product Management
Đăng nhập
ESC

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

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

Bài 21 — Deep Link & Universal Link Testing

Đã đọc 0/13 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

Hãy tưởng tượng bạn làm QA cho một sàn thương mại điện tử như Tiki hoặc Shopee. Bộ phận Marketing gửi một chiến dịch email khuyến mãi Tết cho 2 triệu người dùng, trong email có nút "Xem ưu đãi ngay" dẫn thẳng vào trang sản phẩm iPhone trong app. Người dùng bấm vào, kỳ vọng app mở đúng trang sản phẩm. Nhưng thực tế: app mở lên màn hình Home, hoặc tệ hơn, mở trình duyệt web thay vì app, hoặc app crash. Kết quả là hàng trăm nghìn lượt click bị "rơi", tỷ lệ chuyển đổi tụt thảm hại, và Marketing chỉ tay sang team Engineering. Điều tra ra thì đó là một lỗi deep link chưa được QA test đúng.

Đây chính là lý do Deep Link và Universal Link testing là một kỹ năng cực kỳ giá trị mà nhiều QA mobile bỏ qua. Deep link là cầu nối giữa "thế giới bên ngoài app" (email, SMS, mạng xã hội, quảng cáo, QR code, web) và "một màn hình cụ thể bên trong app". Nếu cây cầu này gãy, mọi nỗ lực marketing, mọi tính năng share, mọi luồng attribution (đo lường nguồn cài đặt) đều sụp đổ. Và điều đáng sợ nhất: deep link là loại lỗi mà bạn gần như không bao giờ phát hiện được nếu chỉ mở app rồi bấm loanh quanh — bởi vì nó luôn được kích hoạt từ bên ngoài app.

Trong bài này, chúng ta sẽ đi sâu vào ba loại link, hiểu cơ chế hoạt động của từng loại trên iOS và Android, và quan trọng nhất là học cách test chúng một cách có hệ thống — từ dòng lệnh adb, xcrun, cho đến việc xử lý những case biên (edge case) mà chỉ QA giỏi mới nghĩ tới.

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

Ba loại link bạn phải phân biệt rõ

Điều đầu tiên và quan trọng nhất: "deep link" là một thuật ngữ ô, bên dưới có ba cơ chế kỹ thuật khác nhau. Nhầm lẫn giữa chúng là nguyên nhân số một khiến QA test sai.

LoạiĐịnh dạng ví dụNền tảngBản chất
Custom URL Schemevietnamcos://course/97Cả iOS & Android (cũ)App tự đăng ký một "scheme" riêng
Universal Link (iOS)https://vietnamcos.com/course/97Chỉ iOSLink HTTPS thật, xác thực qua file trên server
App Link (Android)https://vietnamcos.com/course/97Chỉ AndroidLink HTTPS thật, xác thực qua file trên server
Custom URL Scheme là cơ chế lâu đời nhất. App tự khai báo với hệ điều hành: "Bất cứ URL nào bắt đầu bằng vietnamcos:// hãy giao cho tôi xử lý". Ưu điểm là đơn giản. Nhược điểm chí mạng: bất kỳ app nào cũng có thể đăng ký cùng một scheme (gọi là scheme hijacking — chiếm quyền scheme). Nếu có hai app cùng đăng ký myapp://, hệ điều hành sẽ mở app một cách khó đoán. Ngoài ra, nếu app chưa được cài, click vào vietnamcos://... sẽ chỉ hiện lỗi "không mở được" hoặc chẳng có gì xảy ra — trải nghiệm rất tệ.

Universal Link (iOS) và App Link (Android) ra đời để giải quyết vấn đề trên. Chúng dùng chính URL HTTPS bình thường của website (https://vietnamcos.com/course/97). Điểm hay: cùng một link, nếu app đã cài thì mở app đúng màn hình, nếu app chưa cài thì mở website — không bao giờ để người dùng rơi vào ngõ cụt. Để chống hijacking, cả Apple và Google yêu cầu bạn đặt một file xác thực trên server, chứng minh rằng bạn thực sự sở hữu cả domain lẫn app.

Cơ chế xác thực bằng file trên server

Đây là phần mà QA hay bỏ sót, nhưng nó là gốc rễ của rất nhiều bug.

Trên iOS, bạn phải đặt file apple-app-site-association (viết tắt AASA, không có đuôi mở rộng) tại đường dẫn https://vietnamcos.com/.well-known/apple-app-site-association. File này là JSON, khai báo App ID và những path nào thuộc về app. Khi người dùng cài app lần đầu, iOS tải file này về và ghi nhớ. Từ đó về sau, click vào link HTTPS khớp path sẽ mở app.

Trên Android, tương tự, bạn đặt file assetlinks.json tại https://vietnamcos.com/.well-known/assetlinks.json, khai báo package name và SHA-256 fingerprint của chứng chỉ ký app. Android xác minh file này để bật chế độ "auto-verify", khi đó link mở thẳng app không cần hỏi.

Ba lỗi kinh điển ở tầng này mà QA cần chủ động kiểm tra: (1) File AASA/assetlinks trả về sai Content-Type hoặc bị redirect (Apple không chấp nhận redirect); (2) SHA-256 fingerprint trong assetlinks.json là của bản debug nhưng bản release ký bằng key khác — thế là link chết trên bản Production; (3) File bị cache trên CDN, deploy phiên bản mới nhưng iOS/Android vẫn dùng bản cũ.

Warm start vs Cold start — sự khác biệt sống còn

Một khái niệm mà QA mobile bắt buộc phải nắm: deep link hành xử khác nhau tùy trạng thái app.

  • Cold start: App đang tắt hoàn toàn (không có trong bộ nhớ). Click deep link → hệ điều hành khởi động app từ đầu → deep link phải được "giữ lại" và xử lý sau khi app khởi tạo xong.
  • Warm start: App đang chạy nền (trong recent apps). Click deep link → app được đưa lên foreground và nhận link ngay.
Rất nhiều bug chỉ xuất hiện ở một trong hai trạng thái. Ví dụ phổ biến: developer xử lý deep link trong onNewIntent (Android) hoặc didReceiveURL — chỗ này chỉ được gọi khi warm start. Kết quả là warm start hoạt động hoàn hảo, nhưng cold start thì link bị nuốt mất, app chỉ mở Home. QA nào chỉ test warm start sẽ ký duyệt một quả bom nổ chậm.

Tình huống thực tế

Tình huống 1 — Chiến dịch Grab và cái bẫy cold start

Một team QA tại một công ty gọi xe ở Đông Nam Á (tạm gọi mô hình giống Grab) test tính năng deep link cho voucher. Link grab://promo/tet2026 phải mở thẳng màn hình nhập voucher. Trong suốt sprint, QA test bằng cách: mở app, chuyển sang màn hình khác, rồi dán link vào Notes và bấm. Mọi thứ hoàn hảo, họ pass ticket.

Đến ngày launch chiến dịch, hàng loạt phàn nàn đổ về: người dùng bấm link trong SMS thì app chỉ mở Home, không thấy màn hình voucher đâu. Điều tra ra: QA đã luôn test ở trạng thái warm start (app đang chạy nền). Trong thực tế, người dùng nhận SMS lúc app đã bị tắt hàng giờ — đó là cold start. Code xử lý link nằm sai chỗ, chỉ chạy khi warm.

Bài học: Luôn test deep link ở cả hai trạng thái. Trước mỗi lần test cold start, phải kill app hoàn toàn (vuốt khỏi recent apps hoặc adb shell am force-stop). Ghi rõ trong test case: "Precondition: app đã bị force-stop".

Tình huống 2 — Universal Link chết trên bản Production của một fintech

Một ví điện tử Việt Nam (giả định tương tự MoMo) triển khai Universal Link để khi người dùng bấm link chia sẻ hóa đơn https://pay.example.vn/bill/8891 thì mở thẳng app. Trên bản TestFlight/staging, mọi thứ chạy ngon. Nhưng khi lên App Store, link lại mở Safari thay vì app.

QA trưởng vào cuộc, dùng công cụ kiểm tra file AASA và phát hiện: file apple-app-site-association trên domain production khai báo App ID với Team ID của môi trường dev, trong khi bản Production được ký bằng một Team ID khác. iOS tải file, thấy App ID không khớp app đã cài, nên từ chối mở app và fallback về Safari. Thêm nữa, file này còn bị Cloudflare cache 24 giờ nên sau khi sửa vẫn phải chờ.

Bài học: QA phải kiểm tra file xác thực trên đúng môi trường sẽ phát hành, và phải verify App ID/fingerprint khớp với bản build thật. Đừng bao giờ tin rằng "staging chạy được nghĩa là production chạy được" đối với deep link — vì tầng xác thực server hoàn toàn khác nhau.

Tình huống 3 — Deep link mở nhầm màn hình vì thiếu kiểm tra đăng nhập

Một sàn học trực tuyến (giả định như Vietnamcos) có link https://vietnamcos.com/course/97 mở trang khóa học. QA test khi đã đăng nhập → mở đúng khóa học 97. Ship. Vài ngày sau, một người dùng chưa đăng nhập bấm link từ Facebook. App mở ra, cố render trang khóa học, gọi API cần token, nhận về lỗi 401, và... crash vì màn hình không xử lý trường hợp user == null.

Bài học: Deep link luôn phải được test trong nhiều trạng thái ngữ cảnh của người dùng: đã đăng nhập, chưa đăng nhập, token hết hạn, tài khoản bị khóa. Deep link thường "nhảy cóc" qua các bước điều hướng bình thường (bỏ qua Home, bỏ qua màn hình login), nên nó dễ đưa app vào trạng thái mà developer chưa lường trước. QA giỏi luôn hỏi: "Nếu link này đến tay một người chưa đăng nhập thì sao?".

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

Dưới đây là quy trình test deep link thực chiến, kèm lệnh cụ thể.

Bước 1 — Lập ma trận test. Trước khi chạm máy, liệt kê các chiều cần phủ:

  • Loại link: custom scheme, universal/app link.
  • Trạng thái app: cold start, warm start, đang mở sẵn màn hình khác.
  • Trạng thái cài đặt: app đã cài / chưa cài (test fallback về web/store).
  • Trạng thái người dùng: đã login / chưa login / token hết hạn.
  • Link hợp lệ / link sai định dạng / link trỏ tới resource không tồn tại (ID không có thật).
Bước 2 — Test trên Android bằng adb. Đây là cách nhanh và đáng tin nhất, giả lập đúng việc hệ điều hành gửi Intent:

# Force-stop app trước để test cold start
adb shell am force-stop com.vietnamcos.app

# Gửi deep link (custom scheme)
adb shell am start -W -a android.intent.action.VIEW \
  -d "vietnamcos://course/97" com.vietnamcos.app

# Gửi App Link (HTTPS)
adb shell am start -a android.intent.action.VIEW \
  -d "https://vietnamcos.com/course/97"

Lưu ý: với App Link HTTPS, đừng chỉ định package name — để Android tự quyết định. Nếu nó mở app thì auto-verify đang hoạt động; nếu hiện dialog chọn app hoặc mở browser thì assetlinks.json có vấn đề.

Bước 3 — Kiểm tra trạng thái App Link verification trên Android:

adb shell pm get-app-links com.vietnamcos.app

Lệnh này cho biết mỗi domain đang ở trạng thái verified hay none. Nếu thấy none với domain của bạn, tức là file assetlinks.json không hợp lệ hoặc fingerprint sai.

Bước 4 — Test trên iOS. Với custom scheme trên simulator:

xcrun simctl openurl booted "vietnamcos://course/97"

Với Universal Link, cách này không test đúng cơ chế (nó bỏ qua tầng AASA). Để test thật, cách đáng tin nhất là: gõ link HTTPS vào ứng dụng Notes hoặc Messages trên thiết bị thật, rồi bấm hoặc long-press → "Open in App". Nếu chỉ có option mở Safari mà không có "Open in [App]", nghĩa là Universal Link chưa hoạt động.

Bước 5 — Verify file xác thực trên server:

# iOS
curl -v https://vietnamcos.com/.well-known/apple-app-site-association

# Android
curl -v https://vietnamcos.com/.well-known/assetlinks.json

Kiểm tra: HTTP 200 (không redirect), JSON hợp lệ, App ID/package/fingerprint đúng với bản build. Với Android, Google còn cung cấp Digital Asset Links API để verify tự động.

Bước 6 — Test các case biên: link tới ID không tồn tại (course/99999), link thiếu tham số, link có ký tự đặc biệt hoặc tiếng Việt cần URL-encode, và link cực dài. Quan sát app có xử lý duyên dáng (hiện thông báo lỗi thân thiện) hay crash.

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

  • Chỉ test warm start. Đây là lỗi phổ biến nhất. Luôn force-stop để test cold start riêng.
  • Test bằng cách gõ link vào trình duyệt Chrome trên Android. Chrome có cơ chế riêng và đôi khi chặn việc mở app từ thanh địa chỉ. Kết quả không phản ánh đúng hành vi thực tế. Hãy test qua adb hoặc qua app nhắn tin/ghi chú.
  • Quên test khi app chưa cài. Fallback (mở web hoặc App Store/Google Play với đúng đường dẫn) cũng là một phần của trải nghiệm deep link và phải được kiểm tra.
  • Bỏ qua tầng xác thực server. File AASA/assetlinks phải được verify trên đúng môi trường, đúng build, và lưu ý cache CDN. Đây là nguồn gốc của phần lớn bug "chỉ xảy ra trên Production".
  • Không test click liên tiếp. Bấm nhanh nhiều deep link khác nhau có thể tạo ra chồng chéo navigation stack sai lệch (ví dụ vào khóa học A rồi B, nhưng nút Back lại về Home thay vì A).
  • Mẹo dùng công cụ chính chủ. Apple có Xcode > Diagnostics và bạn có thể xem log Universal Link qua Console.app; Android có pm get-app-links và trang test của Google Digital Asset Links. Dùng chúng thay vì đoán mò.
  • Mẹo kiểm tra attribution. Nếu app dùng dịch vụ như Firebase Dynamic Links, AppsFlyer hay Branch, hãy test cả việc dữ liệu tham số (campaign, source) có được truyền đúng vào analytics không — đây là lý do Marketing quan tâm nhất tới deep link.

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

  • Dựng ma trận test đầy đủ. Chọn một app bạn có (hoặc app mẫu), viết ra bảng test case phủ đủ 5 chiều: loại link, trạng thái app (cold/warm), trạng thái cài đặt, trạng thái đăng nhập, tính hợp lệ của link. Mục tiêu tối thiểu 20 test case.
  • Thực hành lệnh adb. Trên một máy Android (hoặc emulator), dùng adb shell am start để gửi ít nhất 3 deep link khác nhau ở cả hai trạng thái cold và warm. Ghi lại màn hình app mở ra và so với kỳ vọng.
  • Kiểm tra file xác thực. Chọn một app phổ biến ở Việt Nam (Tiki, Shopee, VNExpress...) và dùng curl để xem file apple-app-site-association và assetlinks.json của họ. Phân tích: họ khai báo những path nào thuộc về app? File có trả HTTP 200 không?
  • Săn bug cold start. Cố tình reproduce lỗi "link chỉ hoạt động ở warm start" bằng cách force-stop app rồi mở deep link. Viết một bug report chuẩn với precondition rõ ràng, các bước tái hiện, kết quả kỳ vọng và thực tế.
  • Test case biên. Tạo và test các link "xấu": ID không tồn tại, thiếu tham số, chứa tiếng Việt có dấu chưa encode. Ghi lại app xử lý ra sao và đề xuất hành vi mong muốn.

Tóm tắt

Deep link là cầu nối giữa thế giới bên ngoài và một màn hình cụ thể bên trong app, và nó là nguồn gốc của những lỗi khó chịu nhất vì luôn được kích hoạt từ bên ngoài — nơi mà việc "mở app bấm loanh quanh" không bao giờ chạm tới.

Ba điểm cốt lõi cần khắc sâu: Một, phân biệt rõ ba loại link — custom scheme (đơn giản nhưng dễ bị hijack và không có fallback), Universal Link (iOS) và App Link (Android) — hai loại sau dùng HTTPS thật và cần file xác thực trên server. Hai, luôn test ở cả hai trạng thái cold start và warm start, vì rất nhiều bug chỉ lộ ra ở một trong hai. Ba, deep link "nhảy cóc" qua điều hướng bình thường nên phải test đa dạng ngữ cảnh người dùng (đăng nhập/chưa/hết hạn) và case biên (ID sai, thiếu tham số, app chưa cài).

Về công cụ: adb shell am start và pm get-app-links cho Android, xcrun simctl openurl cùng test thủ công qua Notes/Messages cho iOS, và curl để verify file AASA/assetlinks trên đúng môi trường phát hành. Nắm vững những điều này, bạn không chỉ tránh được thảm họa "chiến dịch marketing rơi hết click" mà còn trở thành QA mà cả team Engineering lẫn Marketing đều tin tưởng.

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