Product Management
Đăng nhập
ESC

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

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

Database Testing Automation

Mở đầu — vì sao bài này quan trọng

Trong hầu hết các dự án phần mềm, dữ liệu là tài sản quý giá nhất. Một bug ở tầng giao diện có thể khiến người dùng khó chịu, nhưng một bug ở tầng database — ví dụ một migration làm mất cột, một ràng buộc khóa ngoại bị thiếu, hay một câu update xóa nhầm hàng nghìn bản ghi — có thể khiến cả công ty thiệt hại nghiêm trọng và không thể khôi phục. Vậy mà trong thực tế, rất nhiều đội QA tại Việt Nam gần như bỏ trắng mảng kiểm thử database. Họ test UI, test API, nhưng phần dữ liệu bên dưới thì "tin tưởng dev" và cầu mong không có gì hỏng.

Đây là một khoảng trống nguy hiểm. Khi bạn test qua UI hoặc API, bạn chỉ nhìn thấy phần nổi của tảng băng. Rất nhiều lỗi dữ liệu ẩn sâu bên dưới: một trigger tính sai số dư, một stored procedure làm tròn tiền không đúng, một migration chạy được trên máy dev nhưng chết trên production vì dữ liệu thật khác dữ liệu giả. Những lỗi này thường không lộ ra ngay, mà âm thầm tích tụ cho đến ngày kế toán đối soát và phát hiện lệch vài trăm triệu.

Bài học này sẽ dạy bạn cách tự động hóa kiểm thử database một cách có hệ thống: từ kiểm thử schema (cấu trúc bảng), kiểm thử tính toàn vẹn dữ liệu (data integrity), cho đến kiểm thử logic nghiệp vụ nằm bên trong database như stored procedure và trigger. Đây là một kỹ năng khiến bạn khác biệt hẳn so với một tester chỉ biết click chuột, và là năng lực cốt lõi của một SDET thực thụ.

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

Database testing automation là việc dùng code và công cụ để tự động kiểm tra rằng dữ liệu được lưu trữ, biến đổi và truy xuất đúng như thiết kế. Chúng ta chia mảng này thành bốn nhóm kiểm thử chính.

1. Schema testing — kiểm thử cấu trúc

Đây là lớp kiểm thử nền tảng: đảm bảo cấu trúc database đúng như thiết kế. Bạn cần kiểm tra rằng bảng tồn tại, các cột có đúng kiểu dữ liệu (type), đúng độ dài (length), đúng ràng buộc cho phép null hay không (nullable), có đúng khóa chính, khóa ngoại, index và ràng buộc unique.

Quan trọng nhất trong nhóm này là migration testing — kiểm tra rằng các script thay đổi cấu trúc database (migration) chạy được sạch sẽ, không lỗi, và có thể rollback được. Đây là nơi rất nhiều sự cố production bắt nguồn: một migration thêm cột NOT NULL mà không có giá trị mặc định sẽ chết ngay khi gặp bảng đã có sẵn dữ liệu.

2. Data integrity testing — kiểm thử tính toàn vẹn

Nhóm này kiểm tra rằng các thao tác insert, update, delete hoạt động đúng và không phá vỡ tính nhất quán của dữ liệu. Ví dụ: khi bạn xóa một khách hàng, các đơn hàng của họ có bị xử lý đúng theo quy tắc (cascade delete, hoặc chặn xóa, hoặc set null)? Khi update số dư ví, giá trị mới có chính xác không? Các ràng buộc unique có thực sự ngăn được dữ liệu trùng lặp khi hai request cùng chạy song song?

3. Business logic trong database

Nhiều hệ thống đặt logic quan trọng ngay trong database: stored procedure tính hoa hồng, trigger tự động cập nhật tồn kho, view tổng hợp báo cáo. Những thành phần này cần được test riêng như test một hàm — cho đầu vào, chạy, kiểm tra đầu ra và các tác dụng phụ (side effect) lên bảng khác.

4. Data quality & consistency testing

Đây là kiểm thử chất lượng dữ liệu ở quy mô lớn: không có bản ghi mồ côi (orphan records — con trỏ tới cha đã bị xóa), không có giá trị âm ở cột số tiền, tổng các dòng chi tiết khớp với dòng tổng, ngày tạo không lớn hơn ngày cập nhật. Loại kiểm thử này đặc biệt quan trọng cho hệ thống báo cáo và data warehouse.

Nguyên tắc vàng: cô lập và tái lập được

Điểm khó nhất của database testing không phải là viết assertion, mà là quản lý trạng thái dữ liệu. Một test tốt phải độc lập (không phụ thuộc kết quả test khác) và tái lập được (chạy 100 lần cho kết quả giống nhau). Muốn vậy, mỗi test phải tự chuẩn bị dữ liệu đầu vào (setup) và dọn dẹp sau khi chạy (teardown), thường bằng cách bọc mỗi test trong một transaction rồi rollback ở cuối, hoặc tạo database sạch mỗi lần bằng Docker.

Tình huống thực tế

Ví dụ 1: Fintech ở TP.HCM và migration "giết" production lúc 2 giờ sáng

Một công ty fintech tại Quận 1, TP.HCM có bảng transactions với khoảng 40 triệu bản ghi. Đội dev viết một migration thêm cột fee_amount DECIMAL(15,2) NOT NULL để lưu phí giao dịch. Trên máy dev với vài trăm dòng dữ liệu giả, migration chạy trong 0.2 giây, xanh mượt. Họ deploy lên production lúc 2 giờ sáng.

Kết quả: migration khóa bảng transactions để thêm cột và điền giá trị mặc định cho 40 triệu dòng. Quá trình này chạy hơn 8 phút, trong khi bảng bị lock hoàn toàn. Mọi giao dịch nạp tiền, chuyển tiền của người dùng đều timeout. Tổng đài nhận hàng trăm cuộc gọi phàn nàn.

Sau sự cố, đội QA thiết lập migration test tự động chạy trên một bản sao dữ liệu production đã ẩn danh (khoảng 40 triệu dòng thật về khối lượng). Mỗi khi có migration mới, pipeline sẽ áp dụng nó lên bản sao này và đo thời gian thực thi cùng thời gian lock bảng. Nếu migration khóa bảng quá 5 giây, test fail và chặn deploy. Từ đó đội chuyển sang chiến lược migration không khóa bảng (thêm cột nullable trước, backfill dữ liệu theo lô, rồi mới thêm ràng buộc). Bài học: schema test phải chạy trên dữ liệu có quy mô giống production, vì hành vi của migration phụ thuộc rất nhiều vào khối lượng dữ liệu thật.

Ví dụ 2: Sàn thương mại điện tử và bug orphan records âm thầm

Một sàn thương mại điện tử ở Hà Nội có bảng ordersorder_items liên kết qua khóa ngoại order_id. Một dev tối ưu hiệu năng đã tắt ràng buộc khóa ngoại (foreign key constraint) trên môi trường production vì nghĩ nó làm chậm insert. Trong ba tháng, một số quy trình hủy đơn xóa bản ghi trong orders nhưng quên xóa dòng tương ứng trong order_items.

Không ai phát hiện, cho đến khi đội tài chính chạy báo cáo doanh thu và thấy con số cao hơn thực tế khoảng 4%, tương đương vài tỷ đồng ghi nhận sai, vì báo cáo cộng cả các order_items mồ côi không còn đơn cha.

Đội QA sau đó viết một bộ data consistency test chạy hàng đêm: một truy vấn đơn giản đếm số dòng order_items không tìm thấy order cha tương ứng (LEFT JOIN ... WHERE orders.id IS NULL). Nếu kết quả khác 0, test fail và gửi cảnh báo lên kênh Telegram của đội. Loại test này cực kỳ rẻ để viết nhưng bắt được những lỗi mà không test nào khác chạm tới. Bài học: đừng chỉ tin vào ràng buộc của database; hãy chủ động viết test kiểm tra tính nhất quán dữ liệu ở cấp toàn bảng, đặc biệt với các bảng có quan hệ cha-con.

Ví dụ 3: Ví điện tử và stored procedure tính sai điểm thưởng

Một ví điện tử phổ biến tại Đông Nam Á dùng một stored procedure calculate_cashback để tính hoàn tiền: 0.5% cho giao dịch dưới 500.000đ, 1% cho giao dịch từ 500.000đ trở lên, tối đa 50.000đ mỗi giao dịch. Logic này nằm hoàn toàn trong database và không hề được test.

Một lần cập nhật, dev sửa điều kiện ranh giới từ >= 500000 thành > 500000. Với giao dịch đúng 500.000đ, hệ thống giờ tính cashback 0.5% thay vì 1%. Sai lệch nhỏ trên mỗi giao dịch nhưng nhân với hàng triệu giao dịch/ngày là con số khổng lồ, và gây khiếu nại từ người dùng.

Đội QA sau đó viết bộ test riêng cho stored procedure bằng pytest kết nối trực tiếp database: chuẩn bị các giao dịch với giá trị biên (499.999đ, 500.000đ, 500.001đ, và một giao dịch cực lớn để test mức trần 50.000đ), gọi procedure, rồi assert kết quả cashback. Bộ test này bắt ngay lỗi ranh giới trước khi lên production. Bài học: mọi logic nghiệp vụ trong database — stored procedure, trigger, function — đều phải được test như code, đặc biệt là các giá trị biên và mức trần/sàn.

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

Hãy dựng một bộ database test tự động cơ bản bằng Python và pytest. Cách này áp dụng được cho PostgreSQL, MySQL/MariaDB — những database phổ biến trong dự án Việt Nam.

Bước 1 — Chuẩn bị môi trường database cô lập. Đừng bao giờ test trên database dev đang dùng chung. Hãy dùng Docker để tạo một database sạch cho mỗi lần chạy test:

docker run --name test-db -e POSTGRES_PASSWORD=test \
  -e POSTGRES_DB=appdb_test -p 5433:5432 -d postgres:15

Bước 2 — Áp dụng migration và kiểm tra schema. Chạy toàn bộ migration lên database test, sau đó kiểm tra cấu trúc bằng cách truy vấn information_schema:

import psycopg2

def test_schema_orders_table(): conn = psycopg2.connect("dbname=appdb_test host=localhost port=5433 user=postgres password=test") cur = conn.cursor() cur.execute(""" SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'orders' """) columns = {row[0]: (row[1], row[2]) for row in cur.fetchall()}

assert columns['total_amount'][0] == 'numeric' assert columns['total_amount'][1] == 'NO' # NOT NULL assert 'created_at' in columns conn.close()

Bước 3 — Test data integrity với transaction rollback. Bọc mỗi test trong transaction để dữ liệu không lưu lại sau khi chạy:

import pytest

@pytest.fixture def db(): conn = psycopg2.connect("dbname=appdb_test host=localhost port=5433 user=postgres password=test") yield conn conn.rollback() # hoàn tác mọi thay đổi conn.close()

def test_cascade_delete_removes_order_items(db): cur = db.cursor() cur.execute("INSERT INTO orders (id, total_amount) VALUES (1, 100000)") cur.execute("INSERT INTO order_items (order_id, product) VALUES (1, 'ao thun')") cur.execute("DELETE FROM orders WHERE id = 1") cur.execute("SELECT COUNT(*) FROM order_items WHERE order_id = 1") assert cur.fetchone()[0] == 0 # con phải bị xóa theo cha

Bước 4 — Test business logic (stored procedure). Gọi procedure và assert kết quả tại các giá trị biên:

@pytest.mark.parametrize("amount, expected", [
    (499999, 2499),    # 0.5%
    (500000, 5000),    # 1%
    (10000000, 50000), # cham tran 50.000
])
def test_calculate_cashback(db, amount, expected):
    cur = db.cursor()
    cur.execute("SELECT calculate_cashback(%s)", (amount,))
    assert cur.fetchone()[0] == expected

Bước 5 — Viết consistency test chạy định kỳ. Những truy vấn kiểm tra toàn cục nên chạy hàng đêm trên bản sao production:

def test_no_orphan_order_items(db):
    cur = db.cursor()
    cur.execute("""
        SELECT COUNT(*) FROM order_items oi
        LEFT JOIN orders o ON oi.order_id = o.id
        WHERE o.id IS NULL
    """)
    assert cur.fetchone()[0] == 0

Bước 6 — Đưa vào CI. Cho toàn bộ test này chạy trong pipeline mỗi lần có thay đổi migration hoặc stored procedure, để phát hiện lỗi trước khi merge.

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

Test phụ thuộc lẫn nhau và không dọn dẹp. Đây là lỗi phổ biến nhất. Một test tạo dữ liệu rồi không xóa, test sau chạy trên dữ liệu bẩn đó và fail ngẫu nhiên. Mẹo: luôn dùng transaction rollback hoặc truncate bảng trong teardown. Mỗi test phải tự chịu trách nhiệm về dữ liệu của mình.

Test trên dữ liệu quá nhỏ so với production. Như ví dụ fintech, migration chạy nhanh với 100 dòng nhưng chết với 40 triệu dòng. Với migration test, hãy chạy trên bản sao có quy mô thật để phát hiện vấn đề hiệu năng và lock.

Quên test rollback của migration. Nhiều đội chỉ test migration đi lên (up) mà quên test đi xuống (down). Khi production gặp sự cố và cần rollback gấp, họ mới phát hiện script down bị lỗi. Hãy test cả hai chiều.

Dùng dữ liệu production thật chưa ẩn danh. Test trên dữ liệu thật là tốt về mặt quy mô nhưng vi phạm quyền riêng tư và Nghị định 13/2023 về bảo vệ dữ liệu cá nhân tại Việt Nam. Luôn ẩn danh (mask) thông tin nhạy cảm như số điện thoại, CMND, số thẻ trước khi đưa vào môi trường test.

Bỏ qua kiểm thử ràng buộc unique dưới điều kiện đồng thời. Một ràng buộc unique có thể trông ổn khi test tuần tự nhưng lộ ra race condition khi hai request insert cùng lúc. Nếu hệ thống của bạn nhạy cảm với việc trùng lặp (ví dụ: tránh tạo hai đơn từ một lần click), hãy viết test chạy song song để kiểm chứng.

Mẹo về seed data. Xây một bộ dữ liệu mẫu (seed) cố định, có phiên bản, dùng chung cho các test. Điều này giúp test dễ đọc và tái lập. Với dữ liệu cần đa dạng và số lượng lớn, có thể sinh tự động — nhưng luôn cố định seed ngẫu nhiên để kết quả tái lập được.

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

  • Schema test. Chọn một bảng trong dự án của bạn (hoặc tạo bảng users). Viết một test truy vấn information_schema để xác nhận bảng có cột email kiểu chuỗi, NOT NULL, và có ràng buộc unique. Cố tình sửa schema sai để xác nhận test fail đúng.
  • Migration test hai chiều. Viết một migration thêm cột phone vào bảng users, kèm script rollback. Viết test tự động: chạy migration up, kiểm tra cột tồn tại; chạy migration down, kiểm tra cột đã biến mất.
  • Data integrity test. Tạo hai bảng ordersorder_items với khóa ngoại cascade delete. Viết test (dùng transaction rollback) xác nhận: khi xóa một order, các order_items của nó bị xóa theo.
  • Business logic test. Viết một stored procedure/function tính phí ship theo bậc (dưới 200.000đ: 30.000đ ship; từ 200.000đ trở lên: miễn phí). Viết test parametrize kiểm tra các giá trị biên 199.999đ, 200.000đ, 200.001đ.
  • Consistency test. Viết một truy vấn phát hiện các bản ghi có updated_at < created_at (một dạng dữ liệu bẩn phổ biến). Đưa nó thành một test tự động, giả lập một dòng bẩn để xác nhận test bắt được.

Tóm tắt

Database testing automation là mảnh ghép mà nhiều đội QA Việt Nam bỏ quên, nhưng lại là nơi những bug tốn kém và khó khôi phục nhất ẩn náu. Bạn đã học bốn nhóm kiểm thử cốt lõi: schema test (cấu trúc bảng và migration), data integrity test (insert/update/delete và ràng buộc), business logic test (stored procedure, trigger, function), và consistency test (phát hiện dữ liệu bẩn ở quy mô toàn bảng).

Nguyên tắc quan trọng nhất là mọi test phải độc lập và tái lập được — dùng transaction rollback hoặc database sạch qua Docker để cô lập trạng thái. Ba tình huống thực tế đã cho thấy bài học sống còn: migration phải test trên quy mô dữ liệu thật, tính nhất quán dữ liệu phải được chủ động kiểm tra thay vì tin vào ràng buộc, và mọi logic trong database phải được test tại giá trị biên.

Khi bạn thành thạo mảng này, bạn không còn là một tester chỉ nhìn phần nổi của tảng băng, mà là một SDET có thể bảo vệ tài sản dữ liệu quý giá nhất của công ty. Hãy bắt đầu ngay với một consistency test đơn giản trên dự án hiện tại của bạn — bạn có thể sẽ bất ngờ với những gì nó phát hiện ra.

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