Tại sao việc kiểm thử cơ sở dữ liệu thường bị bỏ qua
Trong những năm đầu sự nghiệp, tôi đã dành nhiều đêm muộn để sửa các lỗi production mà chẳng liên quan gì đến mã nguồn ứng dụng. Thủ phạm thường là một function trong PostgreSQL đã được ai đó cập nhật mà không nhận ra nó làm hỏng một trigger ở nơi khác.
Từng làm việc với MySQL, PostgreSQL và MongoDB qua nhiều dự án, tôi thấy mỗi loại đều có thế mạnh riêng, nhưng PostgreSQL nổi bật nhờ khả năng xử lý logic phức tạp trực tiếp trong cơ sở dữ liệu thông qua PL/pgSQL. Tuy nhiên, quyền năng càng lớn thì trách nhiệm kiểm thử càng cao.
Hầu hết các lập trình viên tập trung quá nhiều vào unit test cho mã nguồn Python, Java hoặc Node.js nhưng lại coi cơ sở dữ liệu như một “hộp đen”. Chúng ta mặc định rằng nếu các file migration chạy được thì cơ sở dữ liệu vẫn ổn. Tư duy này dẫn đến các hệ thống dễ bị lỗi. pgTAP thay đổi điều này bằng cách đưa giao thức Test Anything Protocol (TAP) trực tiếp vào PostgreSQL, cho phép chúng ta viết test bằng SQL thuần túy.
So sánh các phương pháp kiểm thử cơ sở dữ liệu
Khi nói đến việc xác minh logic cơ sở dữ liệu, có ba con đường phổ biến mà bạn có thể chọn. Mỗi cách có một trường hợp sử dụng cụ thể tùy thuộc vào nơi logic của bạn được đặt.
1. Kiểm thử ở tầng ứng dụng (Cách thông thường)
Phương pháp này sử dụng các framework như Pytest hoặc JUnit để tương tác với cơ sở dữ liệu. Bạn chèn dữ liệu, gọi một function và kiểm tra (assert) trạng thái của cơ sở dữ liệu. Mặc dù hiệu quả cho kiểm thử tích hợp (integration testing), nhưng nó lại chậm vì mỗi test case đều yêu cầu một vòng lặp qua mạng (network round-trip). Nó cũng gây khó khăn khi muốn kiểm thử độc lập các ràng buộc (constraints) hoặc trigger nội bộ.
2. Xác minh thủ công (Cách rủi ro)
Nhiều lập trình viên ít kinh nghiệm thường chạy SELECT * FROM table một cách thủ công sau khi thay đổi. Cách này ổn để kiểm tra nhanh nhưng không thể mở rộng. Nó phụ thuộc vào trí nhớ con người và là nguyên nhân chính gây ra các lỗi hồi quy (regression bugs).
3. Kiểm thử trực tiếp trong DB với pgTAP (Cách chuyên nghiệp)
pgTAP cho phép bạn viết các bài kiểm thử dưới dạng script SQL. Những bài test này chạy trực tiếp bên trong engine của cơ sở dữ liệu. Vì pgTAP sử dụng transaction, mọi thay đổi trong quá trình test sẽ tự động được rollback, giữ cho cơ sở dữ liệu của bạn luôn sạch sẽ. Nó nhanh hơn đáng kể so với kiểm thử ở tầng ứng dụng và cho phép xác minh chi tiết schema, function và quyền truy cập.
Ưu và nhược điểm của pgTAP
Trước khi tích hợp pgTAP vào quy trình làm việc, bạn cần hiểu rõ những điểm mạnh và những rào cản tiềm ẩn của nó.
Ưu điểm
- Tốc độ: Các bài test chạy nội bộ trong engine, loại bỏ độ trễ mạng.
- Sự cô lập: Các bài test được bao bọc trong các transaction. Bạn không cần lo lắng về việc dọn dẹp dữ liệu rác sau khi test.
- Toàn diện: Bạn có thể kiểm tra mọi thứ từ cấu trúc bảng, kiểu dữ liệu của cột cho đến logic trigger phức tạp và quyền hạn người dùng.
- Thân thiện với CI/CD: Nó trả về kết quả chuẩn TAP, dễ dàng được đọc bởi Jenkins, GitHub Actions và GitLab CI.
Nhược điểm
- Đường cong học tập: Bạn cần học một tập hợp các hàm SQL cụ thể do pgTAP cung cấp.
- Thiết lập môi trường: Yêu cầu cài đặt một extension trên PostgreSQL server, điều này có thể bị hạn chế trong một số môi trường cloud được quản lý (mặc dù AWS RDS và các bên khác đã hỗ trợ).
Thiết lập được khuyến nghị
Để bắt đầu, bạn cần hai thứ: extension pgtap trong cơ sở dữ liệu và công cụ dòng lệnh pg_prove (thuộc module Perl TAP::Parser::SourceHandler::pgTAP) để thực thi các bài test.
Cài đặt Extension
Trên hệ thống Debian/Ubuntu, bạn có thể cài đặt extension bằng trình quản lý gói:
sudo apt-get install postgresql-15-pgtap
Sau đó, kích hoạt nó trong cơ sở dữ liệu của bạn:
CREATE EXTENSION pgtap;
Cài đặt pg_prove
Cách dễ nhất để chạy test là thông qua pg_prove. Cài đặt nó bằng CPAN hoặc trình quản lý gói của bạn:
sudo cpan TAP::Parser::SourceHandler::pgTAP
Ngoài ra, nếu bạn thích Docker, many image PostgreSQL đã được cài sẵn pgTAP, hoặc bạn có thể sử dụng một container chuyên dụng cho việc testing để tránh làm bẩn môi trường máy cục bộ.
Hướng dẫn thực hiện: Viết những bài test đầu tiên
Hãy cùng đi qua một kịch bản thực tế. Giả sử chúng ta có một schema thương mại điện tử đơn giản với bảng users và một function tính toán chiết khấu.
1. Kiểm thử Schema
Đầu tiên, chúng ta muốn đảm bảo các bảng và cột tồn tại với đúng kiểu dữ liệu. Điều này ngăn chặn việc vô tình xóa hoặc thay đổi kiểu dữ liệu trong quá trình migration.
-- Tạo file test: test_schema.sql
BEGIN;
SELECT plan(3); -- Chúng ta mong đợi 3 bài test
-- Kiểm tra xem bảng có tồn tại không
SELECT has_table('users');
-- Kiểm tra xem các cột cụ thể có tồn tại không
SELECT has_column('users', 'email');
-- Kiểm tra kiểu dữ liệu của cột
SELECT col_type_is('users', 'email', 'character varying(255)');
SELECT * FROM finish();
ROLLBACK;
2. Kiểm thử Function
Kiểm thử logic bên trong các function là nơi pgTAP thực sự tỏa sáng. Hãy tưởng tượng một function calculate_discount(price numeric) trả về mức chiết khấu 10%.
-- Tạo file test: test_functions.sql
BEGIN;
SELECT plan(2);
-- Kiểm tra mức chiết khấu tiêu chuẩn
SELECT is(
calculate_discount(100.00),
90.00,
'calculate_discount phải trả về 90 cho giá gốc là 100'
);
-- Kiểm tra với giá bằng 0
SELECT is(
calculate_discount(0),
0.00,
'calculate_discount phải xử lý được đầu vào bằng 0'
);
SELECT * FROM finish();
ROLLBACK;
3. Kiểm thử Trigger
Trigger nổi tiếng là khó debug. Với pgTAP, chúng ta có thể kiểm tra xem một trigger có cập nhật chính xác bảng audit log khi email của người dùng thay đổi hay không.
-- Tạo file test: test_triggers.sql
BEGIN;
SELECT plan(1);
-- Chèn một user giả định
INSERT INTO users (id, email) VALUES (1, '[email protected]');
-- Thực hiện cập nhật để kích hoạt trigger
UPDATE users SET email = '[email protected]' WHERE id = 1;
-- Kiểm tra xem bảng audit_log có ghi lại thay đổi không
SELECT results_eq(
'SELECT old_email FROM audit_log WHERE user_id = 1',
$$VALUES ('[email protected]')$$,
'Trigger phải ghi lại email cũ vào audit_log'
);
SELECT * FROM finish();
ROLLBACK;
Chạy các bài kiểm thử
Khi các file test SQL đã sẵn sàng, hãy sử dụng pg_prove để thực thi các bài test. Công cụ này cung cấp một bản tóm tắt rõ ràng về những gì đã vượt qua và những gì thất bại.
pg_prove -d tên_cơ_sở_dữ_liệu_của_bạn test_schema.sql test_functions.sql test_triggers.sql
Kết quả trả về sẽ trông giống như thế này:
test_schema.sql .... ok
test_functions.sql . ok
test_triggers.sql .. ok
All tests successful.
Files=3, Tests=6, 1 wallclock secs
Các best practice khi Unit Test cơ sở dữ liệu
Để duy trì bộ test suite hiệu quả khi dự án phát triển, hãy tuân thủ các nguyên tắc sau:
- Sử dụng Transaction: Luôn bao bọc mã kiểm thử trong
BEGIN;vàROLLBACK;. Điều này đảm bảo dữ liệu test không bao giờ bị lưu lại và cơ sở dữ liệu luôn ở trạng thái xác định. - Tổ chức theo Module: Tạo các file
.sqlriêng biệt cho các phần khác nhau của hệ thống (ví dụ:auth_tests.sql,billing_tests.sql). - Tự động hóa trong CI: Chạy
pg_provenhư một phần của pipeline pull request. Nếu một migration làm hỏng function, CI sẽ báo lỗi trước khi code kịp đẩy lên môi trường staging. - Đừng kiểm thử quá mức: Tập trung vào các logic phức tạp (Function, Trigger, Constraint). Kiểm tra xem
idcó phải là khóa chính hay không thì hữu ích, nhưng kiểm tra từng cột đơn giản có thể là dư thừa đối với các dự án nhỏ.
Bằng cách coi logic cơ sở dữ liệu là mã nguồn quan trọng và áp dụng các nguyên tắc unit test với pgTAP, bạn sẽ giảm thiểu đáng kể rủi ro hỏng dữ liệu và lỗi logic. Việc này có thể tốn thêm chút thời gian ban đầu, nhưng sự an tâm khi triển khai hệ thống vào chiều thứ Sáu là hoàn toàn xứng đáng.

