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ảng | Cơ chế | Dùng cho | Đặc điểm |
|---|---|---|---|
| iOS | UserDefaults | Prefs nhỏ (cờ, cài đặt) | Plain, KHÔNG mã hóa |
| iOS | Keychain | Dữ liệu nhạy cảm (token, password) | Mã hóa, sống qua lần gỡ/cài lại |
| iOS | Core Data / SQLite | Dữ liệu có cấu trúc, danh sách | DB thật, có schema |
| iOS | File system (Documents/Caches) | File, ảnh, cache lớn | Caches có thể bị OS xóa |
| Android | SharedPreferences | Prefs nhỏ | Plain XML, KHÔNG mã hóa |
| Android | EncryptedSharedPreferences | Dữ liệu nhạy cảm | Mã hóa qua Keystore |
| Android | Room / SQLite | Dữ liệu có cấu trúc | DB thật, có migration |
| Android | DataStore | Thay thế SharedPreferences | Async, an toàn hơn |
| Android | Keystore | Khóa mã hóa | Không xuất được key ra ngoài |
| Cross | Realm, MMKV, Hive | DB/KV hiệu năng cao | Phổ biến ở React Native/Flutter |
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?
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.xmlvà 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.
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 pullfile 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.sqlitehoặc.plist. Với Keychain có thể dùng công cụ như Keychain dumper trên thiết bị test.
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ữ.