Product Management
Đăng nhập
ESC

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

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

Bài 55 — Mobile DB & Local Storage Testing

Đã đọ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

Hãy tưởng tượng bạn đang test một ứng dụng ngân hàng số. Người dùng đăng nhập, xem số dư 12.500.000đ, rồi tắt app. Vài phút sau mở lại — số dư hiển thị 0đ, hoặc tệ hơn, hiển thị số dư của tài khoản người dùng trước đó vừa đăng xuất. Không có màn hình crash, không có lỗi mạng, API vẫn trả về đúng. Vấn đề nằm ở một nơi mà nhiều QA thường bỏ qua: dữ liệu lưu trên chính chiếc điện thoại.

Mobile app khác web ở một điểm cốt lõi: nó chạy offline được, nó khởi động lại liên tục, và nó lưu rất nhiều thứ trên thiết bị — token đăng nhập, cache sản phẩm, hàng trong giỏ, bản nháp tin nhắn, cờ cấu hình, thậm chí cả dữ liệu nhạy cảm như số thẻ. Tất cả những thứ đó nằm trong local storage và local database của app. Nếu bạn chỉ test luồng UI mà không kiểm tra tầng lưu trữ, bạn sẽ bỏ sót cả một nhóm bug nghiêm trọng: mất dữ liệu, rò rỉ dữ liệu giữa các phiên đăng nhập, cache lỗi thời, và app hỏng sau khi cập nhật phiên bản.

Bài này tập trung riêng vào kiểm thử tầng lưu trữ cục bộ trên mobile — không đụng tới offline mode logic (Bài 18) hay API testing (Bài 53). Ở đây chúng ta đi sâu vào: dữ liệu được lưu ở đâu, kiểm tra như thế nào, và những cái bẫy khiến QA đau đầu nhất.

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

Các lựa chọn lưu trữ trên mobile

Mỗi nền tảng có nhiều cơ chế lưu trữ, mỗi cơ chế phục vụ một mục đích khác nhau. QA cần biết dữ liệu mình đang test nằm ở đâu để kiểm tra đúng chỗ.

Nền tảngCơ chếDùng choĐặc điểm
iOSUserDefaultsPrefs nhỏ (cờ, cài đặt)Plain, KHÔNG mã hóa
iOSKeychainDữ liệu nhạy cảm (token, password)Mã hóa, sống qua lần gỡ/cài lại
iOSCore Data / SQLiteDữ liệu có cấu trúc, danh sáchDB thật, có schema
iOSFile system (Documents/Caches)File, ảnh, cache lớnCaches có thể bị OS xóa
AndroidSharedPreferencesPrefs nhỏPlain XML, KHÔNG mã hóa
AndroidEncryptedSharedPreferencesDữ liệu nhạy cảmMã hóa qua Keystore
AndroidRoom / SQLiteDữ liệu có cấu trúcDB thật, có migration
AndroidDataStoreThay thế SharedPreferencesAsync, an toàn hơn
AndroidKeystoreKhóa mã hóaKhông xuất được key ra ngoài
CrossRealm, MMKV, HiveDB/KV hiệu năng caoPhổ biến ở React Native/Flutter
Điều quan trọng nhất cần nhớ: UserDefaults và SharedPreferences KHÔNG được mã hóa. Nếu dev lưu token đăng nhập hay số thẻ vào đó, bất kỳ ai có quyền truy cập file (thiết bị đã root/jailbreak, hoặc backup) đều đọc được. Đây là một trong những lỗi bảo mật QA phát hiện nhiều nhất khi kiểm tra tầng lưu trữ.

Ba câu hỏi QA phải trả lời cho mọi dữ liệu lưu cục bộ

Khi test tầng lưu trữ, hãy luôn hỏi ba câu:

  • Ghi (Write): Dữ liệu có được lưu đúng không? Đúng giá trị, đúng kiểu, đúng thời điểm?
  • Đọc & Bền vững (Read & Persistence): Sau khi kill app, khởi động lại máy, hoặc chuyển nền lâu — dữ liệu còn nguyên không?
  • Xóa & Cô lập (Cleanup & Isolation): Khi logout, đổi tài khoản, hoặc xóa app — dữ liệu có được dọn sạch không? Dữ liệu người dùng A có bị lẫn sang người dùng B không?
Câu số 3 là nơi tạo ra những bug nguy hiểm nhất và cũng bị bỏ sót nhiều nhất.

Migration — cái bẫy khi lên phiên bản

Local DB có schema (cấu trúc bảng, cột). Khi dev thêm tính năng mới, họ đổi schema — ví dụ thêm cột phone_verified vào bảng users. App phiên bản mới phải chạy migration để nâng cấp DB cũ của người dùng đang có sang schema mới, mà KHÔNG làm mất dữ liệu.

Đây là kịch bản test cực kỳ quan trọng và hay bị quên: người dùng đang dùng bản 3.1 (có dữ liệu), cập nhật lên bản 3.2 (schema mới). Nếu migration lỗi, app sẽ crash ngay khi mở hoặc mất sạch dữ liệu cũ. Test cài đè (upgrade install) chứ không phải cài mới (fresh install) mới bắt được lỗi này.

Cache invalidation — dữ liệu cũ hiển thị sai

App thường cache dữ liệu để hiển thị nhanh và chạy offline. Nhưng cache phải biết khi nào hết hạn. Nếu giá sản phẩm đổi từ 199.000đ xuống 149.000đ trên server, nhưng app vẫn hiển thị 199.000đ từ cache cũ — đó là bug. QA cần test cả logic làm mới cache lẫn giới hạn dung lượng cache.

Tình huống thực tế

Tình huống 1 — Ví điện tử: số dư "ma" sau khi đổi tài khoản

Một ví điện tử Việt Nam (giả định tên "PayViet") cho phép đổi nhanh giữa nhiều tài khoản mà không cần đăng nhập lại từ đầu. QA test luồng chuyển tiền của tài khoản A: OK. Nhưng khi đổi sang tài khoản B, màn hình lịch sử giao dịch trong 2 giây đầu vẫn hiển thị giao dịch của tài khoản A trước khi được thay bằng dữ liệu đúng.

Nguyên nhân: app lưu cache lịch sử giao dịch vào một file SQLite dùng chung, không gắn user_id vào truy vấn. Khi đổi tài khoản, app tải lại từ server (mất ~1,5s) nhưng trong lúc chờ, nó hiển thị ngay cache cũ — vốn là của người dùng trước.

QA phát hiện bằng cách: sau khi đổi tài khoản, dùng công cụ pull file DB ra máy tính (adb pull với build debug), mở bằng DB Browser for SQLite và thấy dữ liệu tài khoản A vẫn nằm nguyên trong bảng, không hề bị xóa khi logout mềm.

Bài học: Với app đa tài khoản, mọi dữ liệu cục bộ phải được cô lập theo user (thêm cột user_id hoặc dùng file DB riêng cho mỗi user) và xóa/ẩn cache của user cũ ngay lập tức khi đổi phiên. Đây là bug rò rỉ dữ liệu tài chính giữa người dùng — mức độ nghiêm trọng cao nhất.

Tình huống 2 — App thương mại điện tử: crash hàng loạt sau update

Một sàn TMĐT (giả định "ShopNhanh") phát hành bản 4.0 với tính năng "danh sách yêu thích". Dev thêm bảng wishlist và đổi cấu trúc bảng products trong Room DB. QA chỉ test trên máy cài mới bản 4.0 — mọi thứ chạy tốt, release lên Play Store.

24 giờ sau, tỷ lệ crash tăng vọt: 8% người dùng mở app là crash ngay. Điều tra cho thấy những người dùng cũ (đang có bản 3.x với dữ liệu) khi cập nhật lên 4.0, migration DB bị lỗi vì dev quên viết bước migration cho một cột đổi kiểu dữ liệu. App cố mở DB cũ với schema mới → SQLite ném exception → crash.

Nếu QA đã test kịch bản cài đè: cài bản 3.x trước, tạo vài sản phẩm yêu thích và đơn hàng, rồi cài đè bản 4.0 lên — họ đã bắt được crash này trong phòng test thay vì trên 8% người dùng thật.

Bài học: Luôn có một bộ test "upgrade path" — cài phiên bản production hiện tại, tạo dữ liệu thật, rồi cài đè bản mới. Kiểm tra app mở được, dữ liệu cũ còn nguyên, và tính năng mới hoạt động. Fresh install KHÔNG BAO GIỜ đủ để yên tâm release.

Tình huống 3 — App gọi xe: mất bản nháp và token lộ trong backup

Một startup gọi xe ở Đông Nam Á (giả định "GoRide") lưu token đăng nhập vào SharedPreferences thường (không mã hóa) và lưu địa chỉ hay dùng của khách vào file JSON trong thư mục Caches trên iOS.

Hai vấn đề xảy ra:

  • Bảo mật: Một researcher bảo mật lấy file backup của thiết bị (không cần root), đọc thẳng token trong prefs.xml và dùng nó để giả mạo phiên đăng nhập. Đây là lỗi lưu dữ liệu nhạy cảm sai chỗ.
  • Mất dữ liệu: Danh sách địa chỉ nằm trong thư mục Caches — mà iOS có quyền tự động xóa Caches khi máy hết bộ nhớ. Nhiều khách phàn nàn địa chỉ đã lưu tự dưng biến mất.
QA đã có thể phát hiện cả hai nếu kiểm tra: (1) dùng công cụ đọc SharedPreferences/Keychain để xác nhận dữ liệu nhạy cảm được mã hóa và nằm trong Keychain/EncryptedSharedPreferences chứ không phải prefs thường; (2) kiểm tra dữ liệu cần bền vững phải nằm trong Documents chứ không phải Caches.

Bài học: QA phải biết phân loại dữ liệu — nhạy cảm thì phải mã hóa và đặt đúng nơi; cần bền vững thì không được để trong thư mục OS có quyền xóa. Việc này đòi hỏi mở file lưu trữ ra kiểm tra trực tiếp, không chỉ nhìn UI.

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

Đây là quy trình kiểm thử tầng lưu trữ cục bộ cho một tính năng bất kỳ.

Bước 1 — Xác định dữ liệu và nơi lưu. Hỏi dev (hoặc đọc tài liệu kỹ thuật): tính năng này lưu gì, ở đâu? Ví dụ "giỏ hàng lưu trong Room table cart_items", "token lưu trong Keychain". Lập bảng: mỗi mục dữ liệu → cơ chế lưu → có nhạy cảm không → cần bền vững không.

Bước 2 — Chuẩn bị công cụ truy cập lưu trữ. Với build debug:

  • Android: adb shell run-as <package> ls /data/data/<package>/databases/ để xem file DB; adb pull file ra rồi mở bằng DB Browser for SQLite. Xem SharedPreferences trong /shared_prefs/*.xml.
  • iOS: dùng Simulator, tìm thư mục app trong ~/Library/Developer/CoreSimulator/..., mở file .sqlite hoặc .plist. Với Keychain có thể dùng công cụ như Keychain dumper trên thiết bị test.
Bước 3 — Test luồng ghi. Thực hiện hành động (thêm sản phẩm vào giỏ), rồi mở file lưu trữ kiểm tra: bản ghi có xuất hiện không, giá trị có đúng không, có bị lưu trùng không.

Bước 4 — Test tính bền vững. Force kill app (vuốt tắt khỏi recent apps), mở lại → dữ liệu còn không? Khởi động lại điện thoại → còn không? Để app ở nền vài giờ rồi quay lại → còn không?

Bước 5 — Test xóa và cô lập. Logout → dữ liệu nhạy cảm (token) đã bị xóa chưa? Kiểm tra trực tiếp trong file. Đổi tài khoản → dữ liệu user cũ có lẫn sang không? Xóa app rồi cài lại → dữ liệu cũ có "sống lại" không (đặc biệt với Keychain trên iOS, dữ liệu sống qua lần gỡ cài).

Bước 6 — Test migration (upgrade). Cài bản production hiện tại, tạo dữ liệu đầy đủ, cài đè bản mới. Xác nhận: app mở được, dữ liệu cũ còn, tính năng mới chạy.

Bước 7 — Test giới hạn và cache. Đổ dữ liệu lớn (hàng nghìn bản ghi) để kiểm tra hiệu năng và giới hạn dung lượng. Với cache: thay đổi dữ liệu trên server rồi kiểm tra app có làm mới không, cache có hết hạn đúng lúc không.

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

Chỉ test fresh install, bỏ qua upgrade. Đây là lỗi phổ biến nhất và gây hậu quả nặng nhất (crash hàng loạt sau update). Luôn giữ file APK/IPA của bản production để test cài đè.

Không xóa dữ liệu khi logout. Rất nhiều app "đăng xuất" chỉ trên UI mà token, cache vẫn nằm trong storage. Luôn kiểm tra trực tiếp trong file sau khi logout.

Nhầm giữa "kill app" và "clear data". Force kill chỉ tắt tiến trình — dữ liệu vẫn còn. "Clear data" (trong cài đặt Android) mới xóa storage. Test cả hai kịch bản riêng biệt, đừng gộp.

Quên Keychain sống qua lần gỡ cài trên iOS. Người dùng gỡ app, cài lại và bất ngờ vẫn đang đăng nhập. Đây có thể là tính năng (UX tốt) hoặc bug (rủi ro bảo mật với máy dùng chung). Phải làm rõ với product đây là hành vi mong muốn hay không.

Bỏ qua kiểm tra mã hóa. Đừng tin lời "chúng tôi có mã hóa". Mở file prefs.xml hay .plist ra và xác nhận bằng mắt: token phải là chuỗi mã hóa, không phải plaintext.

Không test khi máy đầy bộ nhớ. Thư mục Caches trên iOS bị OS xóa khi thiếu dung lượng. Mô phỏng máy gần đầy để kiểm tra dữ liệu quan trọng không bị đặt nhầm vào Caches.

Mẹo — dùng dữ liệu "bẩn". Nhập tên tiếng Việt có dấu, emoji, chuỗi rất dài, ký tự đặc biệt như dấu nháy đơn (dễ gây lỗi SQL) vào dữ liệu được lưu. Kiểm tra lưu và đọc lại có toàn vẹn không.

Mẹo — reset về trạng thái sạch. Trước mỗi test case về cô lập dữ liệu, đảm bảo bạn bắt đầu từ trạng thái sạch (clear data hoặc gỡ cài) để không bị dữ liệu sót từ test trước làm nhiễu kết quả.

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

Chọn một app trên máy bạn có build debug hoặc một app open-source, thực hiện:

  • Lập bản đồ lưu trữ: Liệt kê ít nhất 3 loại dữ liệu app lưu cục bộ, mỗi loại ghi rõ nằm ở cơ chế nào (SharedPreferences/Keychain/SQLite/file) và có nhạy cảm không.
  • Kiểm tra bền vững: Đăng nhập, thêm dữ liệu (giỏ hàng, yêu thích...), force kill app, khởi động lại điện thoại, mở lại app. Ghi lại: dữ liệu nào còn, dữ liệu nào mất, có đúng kỳ vọng không.
  • Kiểm tra cô lập (nếu app đa tài khoản): Đăng nhập tài khoản A, tạo dữ liệu. Đăng xuất, đăng nhập tài khoản B. Kiểm tra có thấy dữ liệu của A rò rỉ sang B không — trên UI và trong file storage nếu truy cập được.
  • Kiểm tra xóa: Đăng xuất và (nếu có công cụ) mở file storage kiểm tra token đã bị xóa chưa.
  • Viết 5 test case chính thức cho tính năng "giỏ hàng lưu offline", bao phủ: ghi, bền vững qua kill app, cô lập theo user, xóa khi logout, và migration khi cập nhật phiên bản. Mỗi test case ghi rõ bước, dữ liệu chuẩn bị, và kết quả mong đợi.

Tóm tắt

Tầng lưu trữ cục bộ là "vùng mù" của nhiều QA mobile, nhưng lại là nơi ẩn chứa những bug nghiêm trọng nhất: mất dữ liệu, rò rỉ dữ liệu giữa các người dùng, và crash sau khi cập nhật. Hãy nhớ những điểm cốt lõi:

  • Biết dữ liệu nằm ở đâu: UserDefaults/SharedPreferences (nhỏ, KHÔNG mã hóa), Keychain/EncryptedSharedPreferences (nhạy cảm, mã hóa), SQLite/Core Data/Room (có cấu trúc, có migration), file system (Documents bền vững vs Caches có thể bị OS xóa).
  • Ba câu hỏi cho mọi dữ liệu: Ghi đúng không? Bền vững qua kill/restart không? Xóa sạch và cô lập theo user khi logout/đổi tài khoản không?
  • Migration là bắt buộc test: Luôn test cài đè từ bản production hiện tại, không chỉ fresh install.
  • Mở file ra kiểm tra trực tiếp: Đừng tin UI — xác nhận mã hóa và tính đúng đắn của dữ liệu ngay trong file lưu trữ.
Khi bạn đã quen kiểm tra tầng này, bạn sẽ bắt được những lỗi mà test UI thuần túy không bao giờ thấy — và đó chính là khác biệt giữa một QA mobile khá và một QA mobile giỏi.

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