Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn test một ứng dụng ví điện tử. Trên giao diện, bạn nạp 100.000đ, số dư hiển thị đúng 100.000đ, mọi thứ trông hoàn hảo. Bạn đóng bug report với trạng thái "Pass". Nhưng ba tuần sau, kế toán phát hiện trong database có những giao dịch nạp tiền mà số dư người dùng lại không cộng thêm — tiền "bốc hơi" trong bảng transactions nhưng không được ghi vào bảng wallet_balance. Giao diện đẹp, nhưng dữ liệu bên dưới sai.
Đây chính là lý do Database Testing tồn tại. Rất nhiều tester chỉ kiểm thử ở tầng giao diện (UI) mà quên rằng phần lớn logic nghiệp vụ quan trọng — tính tiền, tính điểm, ghi lịch sử, đảm bảo không mất dữ liệu — thực sự xảy ra ở tầng database. UI chỉ là bề mặt. Nếu bạn không nhìn xuống dưới, bạn sẽ bỏ sót cả một lớp lỗi nghiêm trọng mà khách hàng chỉ phát hiện khi đã quá muộn.
Với QA, Database Testing không đòi hỏi bạn phải là một DBA (Database Administrator) chuyên nghiệp. Bạn không cần tối ưu index hay thiết kế schema. Nhưng bạn cần biết viết những câu SQL cơ bản để xác minh rằng dữ liệu được lưu, sửa, xóa đúng cách; rằng các ràng buộc (constraints) hoạt động; và rằng không có dữ liệu "rác" hay "mồ côi" nào sinh ra sau các thao tác nghiệp vụ. Bài này sẽ trang bị cho bạn tư duy và kỹ năng đó — cụ thể, thực tế, áp dụng được ngay.
Khái niệm cốt lõi
Database Testing là quá trình kiểm thử tầng dữ liệu của ứng dụng, tập trung vào việc dữ liệu được lưu trữ, truy xuất và biến đổi có đúng như mong đợi hay không. Nó chủ yếu xoay quanh bốn nhóm kiểm thử chính.
1. Data Integrity — Tính toàn vẹn dữ liệu
Đây là trái tim của Database Testing, xoay quanh các thao tác CRUD (Create, Read, Update, Delete):
- Insert → SELECT: Sau khi thực hiện một hành động trên UI (ví dụ tạo đơn hàng), record có thực sự được ghi vào bảng đúng không? Các cột có mang đúng giá trị không? Ví dụ, bạn tạo đơn hàng 250.000đ, nhưng trong bảng
orders, cộttotal_amountlại lưu250(mất 3 số 0 vì đơn vị lưu là nghìn đồng chẳng hạn) — đó là một bug về đơn vị dữ liệu. - Update → thay đổi đúng: Khi cập nhật, đúng field thay đổi, và chỉ những field đó thay đổi. Một lỗi kinh điển: cập nhật số điện thoại người dùng nhưng vô tình câu UPDATE thiếu điều kiện
WHERE id = ?, khiến toàn bộ bảng bị ghi đè cùng một số điện thoại. - Delete → mất record + kiểm tra FK: Khi xóa, record có thực sự biến mất? Và quan trọng hơn: các bản ghi liên quan qua khóa ngoại (Foreign Key) được xử lý ra sao? Xóa một danh mục sản phẩm thì các sản phẩm thuộc danh mục đó nên bị chặn (RESTRICT), bị xóa theo (CASCADE), hay được set về NULL (SET NULL)? Nếu không xử lý đúng, bạn sẽ có dữ liệu "mồ côi" (orphan records) — sản phẩm trỏ tới một danh mục không còn tồn tại.
2. Constraints & Rules — Ràng buộc dữ liệu
Database thường có sẵn các quy tắc để bảo vệ dữ liệu, và tester phải kiểm tra chúng có được thực thi không:
- NOT NULL: cột bắt buộc không được để trống.
- UNIQUE: email, số điện thoại, mã đơn hàng không được trùng.
- PRIMARY KEY / FOREIGN KEY: khóa chính duy nhất, khóa ngoại phải trỏ tới bản ghi tồn tại.
- CHECK / DEFAULT: ví dụ tuổi phải > 0, trạng thái mặc định là
pending.
3. ACID & Transactions — Giao dịch nguyên tử
Với các nghiệp vụ nhiều bước (chuyển tiền, đặt hàng trừ kho), transaction phải đảm bảo tính atomic — hoặc tất cả cùng thành công, hoặc tất cả cùng thất bại (rollback). Nếu chuyển tiền mà trừ được ở tài khoản A nhưng lỗi khi cộng vào tài khoản B, hệ thống phải hoàn tác (rollback) hoàn toàn, không được để tiền "mất giữa đường". Test transaction thường là test các kịch bản lỗi giữa chừng.
4. Data Migration & Consistency — Nhất quán dữ liệu
Khi hệ thống chạy schema migration (đổi cấu trúc bảng), hoặc khi cùng một thông tin được lưu ở hai nơi (denormalization), tester cần kiểm tra dữ liệu vẫn nhất quán. Ví dụ, orders.customer_name được copy từ customers.name tại thời điểm đặt hàng — hai giá trị này có được đồng bộ đúng logic không?
Công cụ của bạn chủ yếu là các câu SQL cơ bản: SELECT, COUNT, JOIN, GROUP BY, WHERE. Bạn dùng các công cụ như DBeaver, MySQL Workbench, TablePlus, hoặc pgAdmin để kết nối và truy vấn.
Tình huống thực tế
Ví dụ 1 — Sàn TMĐT: đơn hàng "mồ côi" sau khi xóa sản phẩm
Một sàn thương mại điện tử Đông Nam Á (giả định tương tự Tiki) có tính năng cho phép seller xóa sản phẩm khỏi gian hàng. Tester chỉ kiểm tra trên UI: xóa sản phẩm → sản phẩm biến mất khỏi trang → Pass.
Nhưng khi truy vấn database, đội QA phát hiện vấn đề nghiêm trọng. Trong bảng order_items, có tới 1.240 dòng đang trỏ tới các product_id đã bị xóa khỏi bảng products. Câu SQL phát hiện ra điều này rất đơn giản:
SELECT oi.id, oi.product_id
FROM order_items oi
LEFT JOIN products p ON oi.product_id = p.id
WHERE p.id IS NULL;
Kết quả: 1.240 dòng "mồ côi". Hậu quả thực tế là khi khách hàng vào xem lại lịch sử đơn hàng của mình, tên và ảnh sản phẩm hiển thị trống trơn hoặc lỗi "Sản phẩm không tồn tại", dù họ đã thực sự mua và thanh toán món đó.
Bài học rút ra: UI "xanh" không có nghĩa là dữ liệu đúng. Việc xóa cần dùng soft delete (đánh dấu deleted_at thay vì xóa hẳn) hoặc chặn xóa khi còn ràng buộc. Tester phải luôn kiểm tra hành vi của Foreign Key sau các thao tác Delete.
Ví dụ 2 — Fintech: chuyển tiền và bài toán rollback
Một công ty fintech tại TP.HCM phát triển tính năng chuyển tiền nội bộ giữa hai ví. Logic gồm hai bước: trừ tiền ví người gửi, cộng tiền ví người nhận. Tester tạo một kịch bản test đặc biệt: cố tình làm bước cộng tiền thất bại (bằng cách chuyển vào một ví đã bị khóa ở đúng thời điểm giữa hai bước).
Kết quả gây sốc: ví người gửi bị trừ 500.000đ, nhưng ví người nhận không được cộng, và hệ thống báo lỗi "Giao dịch thất bại" cho người dùng. Tiền biến mất. Nguyên nhân là hai lệnh UPDATE không được bọc trong cùng một transaction, nên khi bước hai lỗi, bước một đã commit và không được rollback.
Tester xác minh bằng cách chạy trước và sau:
SELECT user_id, balance FROM wallets WHERE user_id IN (101, 202);
So sánh số dư trước/sau và đối chiếu với bảng transactions, đội QA chứng minh được số tiền tổng trong hệ thống bị giảm — một lỗi tuyệt đối không được phép xảy ra trong tài chính.
Bài học rút ra: Với nghiệp vụ tài chính, luôn test các kịch bản lỗi giữa chừng để kiểm chứng tính atomic của transaction. Tổng số dư toàn hệ thống trước và sau một giao dịch chuyển tiền phải luôn bằng nhau — đây là một "invariant" (bất biến) bạn nên kiểm tra tự động.
Ví dụ 3 — App học trực tuyến: điểm số bị double-count
Một nền tảng học trực tuyến (bối cảnh giống chính dự án này) có tính năng cộng điểm khi học viên hoàn thành bài học. Do lỗi mạng, học viên bấm nút "Hoàn thành" hai lần liên tiếp. Trên UI chỉ hiện một thông báo chúc mừng, nên tester cho là Pass.
Nhưng trong database, bảng lesson_completions xuất hiện hai dòng cho cùng một cặp (user_id, lesson_id), và điểm được cộng gấp đôi. Truy vấn phát hiện:
SELECT user_id, lesson_id, COUNT(*) AS cnt
FROM lesson_completions
GROUP BY user_id, lesson_id
HAVING COUNT(*) > 1;
Có 87 học viên bị cộng điểm trùng, làm sai lệch bảng xếp hạng. Gốc rễ: thiếu ràng buộc UNIQUE trên cặp (user_id, lesson_id).
Bài học rút ra: Luôn kiểm tra các thao tác có thể lặp (double-click, retry, request trùng) tạo ra dữ liệu trùng lặp trong database hay không. Câu GROUP BY ... HAVING COUNT(*) > 1 là "vũ khí" cực kỳ hữu ích để phát hiện duplicate.
Hướng dẫn từng bước
Dưới đây là quy trình thực tế để tiến hành một buổi Database Testing cho một tính năng.
Bước 1 — Xin quyền truy cập database môi trường test. Yêu cầu dev hoặc DBA cấp cho bạn một tài khoản chỉ đọc (read-only) trên môi trường staging/test. Không bao giờ test trên database production trực tiếp. Cài một công cụ như DBeaver hoặc TablePlus để kết nối.
Bước 2 — Hiểu schema liên quan. Trước khi test, xác định những bảng nào sẽ bị ảnh hưởng bởi tính năng. Xem cấu trúc bảng, các cột, kiểu dữ liệu, và đặc biệt là các khóa ngoại. Vẽ nhanh sơ đồ quan hệ giữa các bảng nếu cần.
Bước 3 — Chụp "ảnh trước" (baseline). Trước khi thực hiện thao tác nghiệp vụ trên UI/API, chạy các câu SELECT để ghi lại trạng thái hiện tại. Ví dụ đếm số dòng, ghi lại số dư, ghi lại giá trị các cột quan trọng.
Bước 4 — Thực hiện thao tác nghiệp vụ. Tạo đơn hàng, chuyển tiền, cập nhật hồ sơ... qua đúng luồng người dùng thật.
Bước 5 — Xác minh trên database (verify). Chạy lại các câu SELECT và so sánh với baseline:
- Đúng số record được thêm/sửa/xóa?
- Đúng giá trị ở đúng cột?
- Đúng kiểu dữ liệu và định dạng (ví dụ ngày giờ, số thập phân, encoding tiếng Việt)?
- Không có tác dụng phụ ngoài ý muốn ở bảng khác?
Bước 7 — Kiểm tra dữ liệu mồ côi và trùng lặp. Chạy các câu JOIN kiểm tra orphan records và các câu GROUP BY HAVING kiểm tra duplicate.
Bước 8 — Ghi bug với bằng chứng SQL. Nếu phát hiện lỗi, đính kèm câu SQL đã dùng và kết quả trả về vào bug report. Điều này giúp dev tái hiện và tin tưởng ngay lập tức.
Lỗi thường gặp & mẹo
Lỗi 1: Chỉ test qua UI, tin vào thông báo màn hình. Như các ví dụ trên, UI có thể "nói dối". Luôn xác minh xuống tận database với những tính năng động chạm đến dữ liệu quan trọng.
Lỗi 2: Test trên production hoặc dùng tài khoản có quyền ghi. Rất nguy hiểm — một câu UPDATE thiếu WHERE có thể phá cả bảng. Luôn dùng read-only trên môi trường test.
Lỗi 3: Quên kiểm tra tác dụng phụ ở bảng khác. Một hành động thường ảnh hưởng nhiều bảng. Tạo đơn hàng có thể động tới orders, order_items, inventory, payments, audit_logs. Đừng chỉ nhìn một bảng.
Lỗi 4: Bỏ qua kiểm tra encoding và định dạng. Với dữ liệu tiếng Việt, hãy kiểm tra ký tự có dấu được lưu đúng (UTF-8), không bị thành dấu hỏi ??? hay ký tự lạ. Kiểm tra số tiền có bị làm tròn sai, ngày giờ có bị lệch timezone.
Mẹo: Học thuộc "bộ ba xác minh" gồm ba loại câu truy vấn: (1) SELECT ... WHERE để kiểm tra giá trị cụ thể; (2) LEFT JOIN ... WHERE b.id IS NULL để tìm orphan records; (3) GROUP BY ... HAVING COUNT(*) > 1 để tìm duplicate. Chỉ ba mẫu này đã giúp bạn bắt được phần lớn bug dữ liệu.
Mẹo: Luôn chạy SELECT COUNT(*) trước và sau thao tác. Chênh lệch số dòng là dấu hiệu nhanh nhất cho biết có gì đó được thêm hoặc mất bất thường.
Mẹo: Với dữ liệu nhạy cảm (mật khẩu), xác minh nó được lưu dưới dạng đã băm (hash), không phải plaintext. Đây là một điểm test bảo mật rất cơ bản nhưng thường bị bỏ sót.
Bài tập thực hành
Giả sử bạn đang test một app đặt vé xem phim với các bảng: users, bookings (đặt vé), seats (ghế), payments. Hãy thực hành:
- CRUD cơ bản: Đặt một vé qua UI, sau đó viết câu SELECT xác minh bản ghi trong
bookingscó đúnguser_id,seat_id,showtimevàstatus = 'confirmed'không.
- Kiểm tra trùng lặp: Viết câu truy vấn tìm xem có ghế nào bị đặt hai lần cho cùng một suất chiếu không (gợi ý:
GROUP BY seat_id, showtime_id HAVING COUNT(*) > 1). Đây là lỗi nghiêm trọng nhất của app đặt vé.
- Orphan records: Viết câu JOIN tìm các bản ghi trong
paymentstrỏ tới mộtbooking_idkhông còn tồn tại.
- Transaction: Thiết kế một kịch bản test: nếu bước thanh toán thất bại sau khi ghế đã được đánh dấu "đã đặt", hệ thống có nhả ghế ra (rollback) để người khác đặt được không? Mô tả các câu SELECT bạn sẽ dùng để chứng minh.
- Constraint: Thử đặt vé cho một
seat_idkhông tồn tại (qua API). Database có từ chối bằng lỗi Foreign Key không?
Tóm tắt
Database Testing giúp bạn nhìn xuống dưới lớp giao diện để đảm bảo dữ liệu — thứ có giá trị thực sự với doanh nghiệp — được lưu, sửa, xóa đúng đắn. Là QA, bạn không cần trở thành DBA, nhưng cần thành thạo SQL cơ bản để tự xác minh.
Những điểm cốt lõi cần nhớ: kiểm tra tính toàn vẹn dữ liệu qua CRUD (Insert/Update/Delete và ảnh hưởng của Foreign Key); kiểm tra các ràng buộc NOT NULL, UNIQUE, FK; kiểm tra tính atomic của transaction với các kịch bản lỗi giữa chừng; và luôn săn tìm hai loại "bệnh" phổ biến là orphan records và duplicate records. Ba mẫu câu truy vấn — kiểm tra giá trị, tìm mồ côi bằng LEFT JOIN, tìm trùng bằng GROUP BY HAVING — sẽ theo bạn suốt sự nghiệp. Quan trọng nhất: đừng bao giờ tin UI một cách mù quáng. Dữ liệu đúng mới là bằng chứng cuối cùng của một tính năng hoạt động đúng.