Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang test một ứng dụng ví điện tử. Người dùng nạp tiền vào ví, thấy số dư tăng lên ngay, rồi khóa màn hình bỏ điện thoại vào túi. Mười lăm phút sau, một giao dịch hoàn tiền từ đối tác đáng lẽ phải được đồng bộ về nhưng lại không xuất hiện — bởi vì hệ thống dựa vào một background task chạy khi app không mở, và task đó… không bao giờ chạy. Đến khi người dùng mở lại app, số dư lệch, và họ gọi lên tổng đài phàn nàn.
Đây chính là dạng lỗi mà Background Tasks & Sync Testing sinh ra để phòng ngừa. Phần lớn tester chỉ kiểm thử app khi nó đang ở foreground — tức là màn hình đang hiển thị, người dùng đang tương tác. Nhưng một ứng dụng di động hiện đại làm rất nhiều việc khi bạn không nhìn nó: tải tin nhắn mới, đồng bộ dữ liệu offline lên server, làm mới nội dung feed, tải ảnh, gửi analytics, cập nhật vị trí. Nếu những công việc nền này chạy sai, chạy trễ, hoặc không chạy, người dùng sẽ thấy dữ liệu cũ, mất dữ liệu, hoặc pin tụt bất thường — mà bug report thường rất mơ hồ kiểu "app không cập nhật".
Cái khó của mảng này là hệ điều hành chủ động chống lại bạn. iOS và Android đều siết chặt việc chạy nền để tiết kiệm pin, nên background task không phải muốn chạy là chạy — nó bị OS quyết định thời điểm, và trong nhiều trường hợp có thể bị bỏ qua hoàn toàn. Là QA, bạn phải hiểu đủ sâu về các cơ chế này để biết khi nào task nên chạy, làm sao ép nó chạy để test, và điều gì xảy ra khi nó không chạy. Bài học này tập trung riêng vào chủ đề đó — không lẫn với offline mode (Bài 18) hay lifecycle events chung (Bài 42), mà đi thẳng vào các tác vụ nền và luồng đồng bộ.
Khái niệm cốt lõi
Background trên iOS — bốn cơ chế bạn phải phân biệt
iOS quản lý background rất nghiêm ngặt. Có bốn cơ chế chính mà QA cần nắm:
- Background Fetch (làm mới cơ hội): OS thỉnh thoảng đánh thức app trong vài chục giây để nó tải nội dung mới, dựa trên thói quen sử dụng của người dùng. Bạn không kiểm soát được thời điểm chính xác — có thể vài phút, có thể vài giờ một lần. App có tối đa khoảng 30 giây để hoàn thành, nếu quá thì bị kill.
- Background Processing (
BGTaskScheduler/BGProcessingTask): dành cho tác vụ dài, có thể trì hoãn như đồng bộ database lớn, dọn dẹp cache, training model on-device. iOS thường chỉ chạy loại này khi thiết bị đang sạc và kết nối Wi-Fi. Đây là API hiện đại thay thế cho background fetch cũ. - Silent Push (push không hiển thị): server gửi một push với
content-available: 1, đánh thức app để tải dữ liệu mà không hiện thông báo. iOS bóp throttle loại này khá mạnh — gửi quá nhiều có thể bị bỏ qua. - Background URLSession: cơ chế tải/upload file chạy độc lập, do daemon của hệ thống quản lý, tiếp tục ngay cả khi app bị kill.
Background trên Android — WorkManager và các ràng buộc
Phía Android, kể từ các phiên bản gần đây, cách chuẩn để làm việc nền có thể trì hoãn là WorkManager. Nó cho phép định nghĩa công việc kèm constraints (ràng buộc): chỉ chạy khi có mạng, khi đang sạc, khi pin không thấp, khi thiết bị idle. WorkManager tự động chọn cơ chế bên dưới (JobScheduler…) và đảm bảo công việc cuối cùng sẽ chạy kể cả sau khi app bị kill hoặc thiết bị khởi động lại.
Bên cạnh đó có:
- Foreground Service: cho tác vụ người dùng thấy được và cần chạy liên tục (ghi âm, chạy bộ theo GPS, phát nhạc) — bắt buộc hiện notification.
- Doze Mode & App Standby: khi màn hình tắt và thiết bị không cử động một thời gian, Android gom các background task vào các "cửa sổ bảo trì" (maintenance windows) thưa dần. Đây là nguồn gốc số một của bug "sync bị trễ hàng giờ".
- Battery Optimization / hạn chế của nhà sản xuất: Xiaomi (MIUI), Oppo, Vivo, Samsung ở thị trường Việt Nam nổi tiếng "sát thủ background" — họ tự kill app nền rất hung để tiết kiệm pin. Một app đồng bộ hoàn hảo trên Pixel có thể chết ngóm trên Redmi.
Sync — nhiều hơn là "gọi API"
Đồng bộ (sync) là quá trình hòa hợp dữ liệu giữa thiết bị và server. QA cần kiểm tra các khía cạnh:
- Chiều đồng bộ: chỉ tải xuống (pull), chỉ đẩy lên (push), hay hai chiều?
- Conflict resolution (giải quyết xung đột): nếu cùng một bản ghi bị sửa ở hai nơi, luật nào thắng? "Last write wins", merge, hay hỏi người dùng?
- Tính idempotent: nếu một sync request được gửi lại (do retry), server có tạo bản ghi trùng không?
- Retry & backoff: khi mạng lỗi, app thử lại theo chiến lược nào? Có "exponential backoff" để tránh dội bom server không?
- Trạng thái sync hiển thị cho người dùng: đang đồng bộ, đồng bộ xong, đồng bộ thất bại — có rõ ràng không?
Tình huống thực tế
Tình huống 1 — App ghi chú và cú vuốt tắt định mệnh
Một startup ở TP.HCM làm app ghi chú tương tự Notion cho thị trường Việt. Kiến trúc offline-first: người dùng gõ ghi chú, lưu local, một background sync đẩy lên server sau. Trong sprint demo, mọi thứ chạy mượt. Nhưng khi lên TestFlight với người dùng thật, họ nhận được nhiều báo cáo "mất ghi chú".
QA vào cuộc và tái hiện được: người dùng gõ ghi chú → vuốt tắt app ngay lập tức (thói quen rất phổ biến của người Việt để "đóng app cho đỡ tốn pin") → mở lại thì ghi chú biến mất trên các thiết bị khác. Nguyên nhân: đội dev dùng một background task thường để đẩy sync, nhưng khi app bị force-quit, iOS hủy task đó, và dữ liệu chưa kịp lên server. Local vẫn còn nhưng chưa đồng bộ, đến khi cache bị dọn thì mất luôn.
Bài học: luôn test kịch bản "thao tác rồi force-quit ngay". Với dữ liệu quan trọng, đừng chỉ dựa vào background sync có thể bị hủy — cần persist local bền vững trước, và ưu tiên đẩy sync ngay khi còn ở foreground hoặc dùng background URLSession vốn sống sót qua force-quit. Sau khi sửa, con số "mất ghi chú" trong bug tracker giảm từ 23 báo cáo/tuần xuống 0.
Tình huống 2 — Sync trễ 6 tiếng trên điện thoại Xiaomi
Một app giao vận (logistics) cho tài xế ở khu vực Đông Nam Á cần đồng bộ danh sách đơn hàng mới về máy tài xế mỗi 15 phút bằng WorkManager với constraint "có mạng". QA test trên máy Pixel và Samsung flagship: hoàn hảo, đúng 15 phút một lần. Team tự tin release.
Vài ngày sau, tổng đài ở Việt Nam báo tài xế dùng Redmi Note "không thấy đơn mới suốt cả buổi sáng". QA mượn đúng một chiếc Redmi để test và phát hiện: MIUI đưa app vào diện "battery optimization" mặc định cực gắt, khi màn hình tắt và tài xế để điện thoại trong túi, MIUI kill hẳn app nền, WorkManager không có cơ hội chạy. Sync chỉ diễn ra khi tài xế chủ động mở app — có khi trễ tới 6 tiếng.
Bài học: ma trận thiết bị test phải bao gồm các dòng máy phổ biến thực tế của người dùng, không chỉ máy flagship. Với thị trường Việt Nam, đó là Xiaomi, Oppo, Vivo, Samsung tầm trung. Giải pháp cuối cùng gồm ba phần: (1) hướng dẫn tài xế tắt battery optimization cho app kèm màn hình onboarding, (2) chuyển các đơn khẩn sang silent push để chủ động đánh thức thay vì chờ WorkManager, (3) dùng foreground service khi tài xế đang trong ca làm việc active. Đây là ví dụ kinh điển vì sao QA cần hiểu hành vi OEM.
Tình huống 3 — Push trùng lặp vì sync không idempotent
Một sàn thương mại điện tử có tính năng "đồng bộ giỏ hàng" giữa app và web. Khi mạng chập chờn (rất thường gặp khi người dùng đi thang máy hoặc vào hầm để xe), app retry request sync giỏ hàng. QA để ý trong một phiên test trên mạng 3G yếu: cùng một món hàng bị thêm vào giỏ ba lần.
Đào sâu, hóa ra request đầu thực ra đã tới server thành công nhưng response bị mất do mạng rớt, nên client tưởng thất bại và retry. Server không kiểm tra idempotency key nên tạo bản ghi mới mỗi lần. Trên background sync này lỗi càng khó phát hiện vì nó diễn ra âm thầm.
Bài học: test sync phải bao gồm kịch bản "request thành công nhưng response mất". Dùng công cụ giả lập mạng (network conditioner) để chèn packet loss ở chiều response, hoặc dùng proxy để nuốt response sau khi server đã xử lý. Yêu cầu dev thêm idempotency key cho mọi mutation qua sync. Sau khi fix, tỷ lệ giỏ hàng lỗi trên phân khúc mạng yếu giảm rõ rệt.
Hướng dẫn từng bước
Đây là quy trình thực chiến để test một luồng background/sync bất kỳ:
Bước 1 — Lập bản đồ các background task. Ngồi với dev hoặc đọc code/tài liệu để liệt kê: app có những task nền nào, dùng cơ chế gì (background fetch, BGTaskScheduler, WorkManager, foreground service, silent push), trigger khi nào, và ràng buộc là gì. Không thể test cái bạn không biết tồn tại.
Bước 2 — Xác định "định nghĩa đúng" cho mỗi task. Với mỗi task, viết ra: kết quả mong đợi, thời gian tối đa chấp nhận được (SLA), và điều gì xảy ra khi task thất bại (fail-safe). Ví dụ: "sync đơn hàng phải hoàn tất trong ≤ 15 phút khi có mạng; nếu thất bại 3 lần liên tiếp, hiện banner cảnh báo".
Bước 3 — Ép task chạy để test (đừng ngồi chờ OS). Trên iOS, dùng Xcode để trigger thủ công background fetch/processing (Debug menu có tùy chọn simulate background fetch; với BGTaskScheduler có lệnh LLDB để launch task theo identifier). Trên Android, dùng adb để ép WorkManager chạy hoặc đẩy thiết bị vào Doze mode bằng lệnh adb shell dumpsys deviceidle force-idle, rồi kiểm tra hành vi.
Bước 4 — Test các điều kiện biên của OS. Bật/tắt Low Power Mode (iOS) và Battery Optimization (Android). Ép vào Doze/App Standby. Test khi app ở background, khi bị suspend, và khi bị force-quit. Ghi lại task nào sống, task nào chết trong từng trạng thái.
Bước 5 — Chèn lỗi mạng vào luồng sync. Dùng network conditioner để tạo mạng chậm, mất gói, timeout. Đặc biệt test kịch bản "response bị mất sau khi server đã xử lý" để bắt lỗi idempotency. Kiểm tra retry/backoff có hoạt động và không dội bom server.
Bước 6 — Kiểm tra tính toàn vẹn dữ liệu sau sync. So sánh dữ liệu client và server. Tạo xung đột có chủ đích (sửa cùng bản ghi ở hai nơi) và xác nhận luật conflict resolution chạy đúng. Chạy sync nhiều lần liên tiếp để chắc không sinh bản ghi trùng.
Bước 7 — Đo tác động pin và tài nguyên. Dù chủ đề pin có bài riêng, ở đây bạn vẫn cần xác nhận background task không chạy vô tội vạ (ví dụ vòng lặp retry sai gây thức máy liên tục). Xem log để biết task chạy bao nhiêu lần trong một khoảng thời gian.
Bước 8 — Ghi log và bằng chứng. Với mỗi bug, đính kèm: thiết bị/OS, trạng thái app, các lệnh adb/LLDB đã dùng, log server chứng minh request đến hay không. Background bug rất khó reproduce nên bằng chứng rõ ràng là vàng.
Lỗi thường gặp & mẹo
Lỗi: chỉ test trên foreground. Đây là sai lầm phổ biến nhất. Nếu bạn chưa từng khóa màn hình, chuyển app ra nền, hay force-quit trong lúc test sync, coi như bạn chưa test background gì cả.
Lỗi: ngồi chờ OS tự chạy task. Background fetch có thể vài giờ mới chạy một lần. Đừng bao giờ chờ — luôn dùng công cụ trigger thủ công (Xcode, adb, LLDB). Nếu không, bạn sẽ không bao giờ test đủ số lượt.
Lỗi: bỏ quên các dòng máy OEM phổ biến ở Việt Nam. Như tình huống 2 cho thấy, Xiaomi/Oppo/Vivo có hành vi kill background rất khác máy Google. Test ma trận thiết bị phản ánh đúng người dùng thật.
Lỗi: coi background sync là kênh tin cậy tuyệt đối cho dữ liệu quan trọng. OS có thể trì hoãn hoặc bỏ qua task. Với dữ liệu không được phép mất (giao dịch, ghi chú), cần persist local bền và có cơ chế đẩy chủ động, không phó mặc cho background.
Mẹo: thêm một "debug panel" ẩn trong app hiển thị lần sync gần nhất, kết quả, số lần retry. Điều này giúp QA quan sát trạng thái background mà không cần đào log — cực kỳ tiết kiệm thời gian.
Mẹo: ghi lại timestamp server-side cho mỗi sync request. Khi tranh luận "app không sync" với dev, log server là bằng chứng khách quan cho biết request có thực sự đến hay không, cắt đứt vòng đổ lỗi qua lại.
Mẹo: với silent push, nhớ rằng iOS throttle rất mạnh. Đừng thiết kế test giả định "cứ gửi push là app thức" — hãy test cả kịch bản push bị OS bỏ qua và xác minh app vẫn có đường phục hồi (ví dụ sync bù khi mở app).
Bài tập thực hành
- Lập bản đồ background của một app thật. Chọn một app bạn hay dùng (ví dụ một app ngân hàng hoặc mạng xã hội). Liệt kê tối thiểu ba tác vụ nền bạn nghĩ nó thực hiện, đoán cơ chế OS tương ứng, và viết SLA mong đợi cho mỗi cái.
- Force-quit test. Với một app có tính năng lưu/đồng bộ, thực hiện thao tác tạo dữ liệu rồi force-quit ngay lập tức. Kiểm tra dữ liệu có còn và có đồng bộ sang thiết bị/web khác không. Ghi lại kết quả như một bug report chuẩn.
- Doze mode trên Android. Dùng
adb shell dumpsys deviceidle force-idleđể đẩy một app vào Doze, sau đó quan sát xem sync định kỳ còn chạy không và trễ bao lâu. Viết lại các lệnh và quan sát.
- Kịch bản response mất. Mô tả chi tiết cách bạn sẽ dựng một test để tái hiện lỗi "request thành công nhưng response bị mất", và bạn sẽ kiểm tra điều gì để phát hiện lỗi idempotency.
- Thiết kế test matrix. Với một app đồng bộ dữ liệu, hãy viết ma trận test gồm các trục: trạng thái app (foreground/background/suspended/force-quit) × điều kiện OS (bình thường/Low Power/Doze) × điều kiện mạng (ổn/chậm/mất). Đánh dấu những ô ưu tiên cao nhất và giải thích vì sao.
Tóm tắt
Background Tasks & Sync Testing là mảng kiểm thử mà app làm việc khi người dùng không nhìn — và cũng là nơi hệ điều hành chủ động cản trở bạn để tiết kiệm pin. Trên iOS, bạn cần phân biệt Background Fetch (cơ hội), Background Processing qua BGTaskScheduler (dài, trì hoãn được), silent push và background URLSession — đồng thời nhớ rằng force-quit và Low Power Mode có thể vô hiệu hóa chúng. Trên Android, WorkManager với constraints là công cụ chuẩn, nhưng Doze mode và đặc biệt là các "sát thủ background" của Xiaomi/Oppo/Vivo có thể giết chết sync trong thực tế Việt Nam.
Với sync, đừng chỉ kiểm tra "gọi API thành công" — hãy soi conflict resolution, tính idempotent, retry/backoff, và toàn vẹn dữ liệu hai chiều. Ba tình huống trong bài — mất ghi chú vì force-quit, sync trễ 6 tiếng trên Redmi, giỏ hàng trùng vì response mất — đều là những bug thật, khó phát hiện nếu chỉ test foreground trên máy flagship. Nguyên tắc vàng: ép task chạy thay vì ngồi chờ, test đủ điều kiện biên của OS và OEM, và luôn có bằng chứng log server. Nắm vững mảng này, bạn sẽ bắt được cả một lớp bug mà phần lớn tester không bao giờ chạm tới.