DataOps 4 — Kiểm thử & chất lượng trong pipeline
Vì sao kiểm thử là trái tim DataOps
Trong phát triển phần mềm truyền thống, "test" đã là văn hoá. Nhưng đội dữ liệu ngân hàng lâu nay hay bỏ qua: viết SQL, chạy tay một lần thấy "trông ổn", rồi đẩy vào báo cáo. Vấn đề là dữ liệu không đứng yên — nguồn thay đổi ngoài tầm kiểm soát của bạn: hệ thống core banking đổi format, một trường bỗng đầy NULL, một mã tiền tệ lạ xuất hiện, số dòng tụt một nửa vì job upstream lỗi. Code không đổi mà báo cáo vẫn sai.
Kiểm thử trong DataOps giải quyết đúng hai nỗi lo này:
- Bắt lỗi sớm (fail fast). Phát hiện dữ liệu xấu ở đầu vào rẻ hơn nhiều so với khi phòng tuân thủ phát hiện số CAR sai trong báo cáo gửi NHNN.
- Tự tin thay đổi (change with confidence). Có một bộ test xanh nghĩa là bạn dám sửa logic, refactor model, đổi nguồn — vì nếu làm hỏng, test sẽ đỏ ngay chứ không âm thầm sai suốt ba tháng.
Bài này thuộc series DataOps, nối tiếp phần version control & code review. Khi code đã ở Git, test là thứ chạy trên mỗi thay đổi — nền tảng cho CI/CD ở bài sau. Bài này cũng phân định ranh giới với observability ở bài 6, và bổ trợ cho gov-03 chất lượng dữ liệu.
Hai loại kiểm thử đặc thù của dữ liệu
Điểm mấu chốt mà nhiều người nhầm: trong pipeline dữ liệu có hai loại test khác bản chất, cần cả hai.
1. Test CODE / logic — giống phần mềm
Đây là unit test cho hàm biến đổi và logic pipeline. Bạn cố định input, chạy hàm, so với output kỳ vọng. Dữ liệu ở đây là dữ liệu giả (fixture) do bạn dựng, không phải dữ liệu thật.
Ví dụ: hàm phân loại nhóm nợ theo số ngày quá hạn. Bạn viết test khẳng định "0 ngày → nhóm 1", "95 ngày → nhóm 3", "biên 90 ngày → nhóm 2 hay 3?". Test này chỉ phụ thuộc code, không phụ thuộc dữ liệu production. Nó xanh mãi trừ khi ai đó sửa logic sai. Đây chính xác là unit test như phần mềm.
2. Test DỮ LIỆU (data tests) — không có trong phần mềm thuần
Loại này kiểm dữ liệu thực có đúng kỳ vọng không. Code có thể đúng hoàn hảo nhưng dữ liệu vẫn vi phạm giả định. Vì dữ liệu thay đổi ngoài kiểm soát, ta phải liên tục khẳng định lại các bất biến (invariant) của nó.
| Loại data test | Kiểm điều gì | Ví dụ ngân hàng |
|---|---|---|
| Schema | Cột tồn tại, đúng kiểu | balance phải là NUMERIC, không phải TEXT |
| Not-null | Trường bắt buộc không rỗng | customers.city không được NULL với KH mở mới |
| Unique | Không trùng khoá | accounts.account_no là duy nhất |
| Accepted values | Giá trị nằm trong tập cho phép | accounts.currency ∈ {VND, USD, EUR} |
| Referential / quan hệ | Khoá ngoại có bản ghi cha | Mọi transactions.account_id phải khớp accounts.id |
| Range / hợp lệ | Giá trị trong khoảng hợp lý | balance >= 0 cho tài khoản tiết kiệm |
| Freshness | Dữ liệu đủ mới | Giao dịch mới nhất phải trong 24h qua |
| Volume / row count | Số dòng nằm trong biên | Số giao dịch/ngày không tụt >50% so với trung bình |
| Phân phối | Không lệch bất thường | Tỷ lệ giao dịch kind='refund' không đột biến |
Sự khác nhau cốt lõi:
Công cụ triển khai data test
Không cần tự viết framework — hệ sinh thái đã đủ chín:
- dbt tests — nếu đội đã dùng dbt, đây là lựa chọn đầu tiên. Hai dạng: generic test (khai báo
not_null,unique,accepted_values,relationshipsngay trongschema.yml) và singular test (một file.sqlchứa truy vấn — trả về dòng nào tức là dòng đó vi phạm). Chi tiết ở dbt-04 tests & docs. - Great Expectations (GE) — Python-native, khai báo "expectations" phong phú, sinh Data Docs trực quan. Mạnh khi pipeline không dựa trên dbt (ví dụ Spark/pandas).
- Soda — định nghĩa check bằng SoL (Soda Checks Language) gọn gàng, hợp cho đội thiên monitoring, tích hợp cảnh báo tốt.
Minh hoạ generic test bằng dbt (đây là YAML cấu hình, không phải block chạy trên sandbox):
# models/marts/schema.yml — minh hoạ, không chạy trên sandbox
models:
- name: dim_accounts
columns:
- name: account_no
tests: [not_null, unique]
- name: currency
tests:
- accepted_values:
values: ['VND', 'USD', 'EUR']
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: id
Data test thực — chạy được trên sandbox
Về bản chất, một data test chỉ là một câu SELECT tìm bản ghi vi phạm: nếu nó trả về 0 dòng → PASS, trả về >0 dòng → FAIL. Dưới đây là các data test thật chạy được trên sandbox PostgreSQL read-only của Knowledge Base.
Test not-null — tìm khách hàng thiếu city (mỗi dòng trả về là một vi phạm):
-- ▶ Chạy được
SELECT id, full_name
FROM customers
WHERE city IS NULL;
Test uniqueness — so tổng số dòng với số account_no phân biệt; nếu chênh lệch >0 nghĩa là có trùng khoá:
-- ▶ Chạy được
SELECT COUNT(*) AS tong_dong,
COUNT(DISTINCT account_no) AS so_account_no_phan_biet,
COUNT(*) - COUNT(DISTINCT account_no) AS so_trung
FROM accounts;
Test referential integrity — giao dịch "mồ côi" không khớp tài khoản nào (LEFT JOIN, giữ dòng mà bên accounts là NULL):
-- ▶ Chạy được
SELECT t.id, t.account_id, t.amount
FROM transactions t
LEFT JOIN accounts a ON t.account_id = a.id
WHERE a.id IS NULL;
Test range — tài khoản có số dư âm (vi phạm bất biến balance >= 0):
-- ▶ Chạy được
SELECT id, account_no, balance, currency
FROM accounts
WHERE balance < 0;
Trong dbt, bạn gói đúng các truy vấn kiểu này thành singular test trong thư mục tests/, và dbt tự chạy chúng trên mỗi dbt test. Ý tưởng giống hệt — chỉ khác là được tự động hoá và báo cáo tập trung.
Test theo VỊ TRÍ trong pipeline — circuit breaker
Có test đúng chưa đủ; phải đặt chúng đúng chỗ. Nguyên tắc: dựng các chốt kiểm (checkpoint) tại các điểm chuyển giao trong pipeline, để dữ liệu xấu không lan xuống hạ nguồn. Đây là ý tưởng circuit breaker — như cầu dao điện, khi phát hiện bất thường thì ngắt mạch, chặn dữ liệu bẩn lây nhiễm báo cáo cuối.
Ba chốt điển hình:
- Đầu vào — source freshness & schema. Trước khi biến đổi bất cứ thứ gì, kiểm nguồn có đủ mới và đúng cấu trúc không. Nếu bảng giao dịch chưa cập nhật hôm nay, dừng ngay — chạy tiếp chỉ sinh ra báo cáo cũ mạo danh báo cáo mới.
- Sau transform. Sau khi tạo bảng marts/fact, kiểm các bất biến: not-null trên khoá, uniqueness, referential, range. Đây là nơi bắt lỗi logic biến đổi thể hiện ra dữ liệu.
- Trước khi phục vụ. Trước khi mở cho BI/báo cáo/API, kiểm volume (số dòng hợp lý), đối soát tổng, control totals. Đây là cầu dao cuối cùng trước khi số liệu tới tay người dùng tuân thủ.
Shift-left: test sớm ở PR/CI
Đặt test cuối pipeline vẫn còn muộn — lỗi đã tiêu tốn tài nguyên chạy. Triết lý shift-left đẩy kiểm thử về càng sớm càng tốt: chạy test ngay trên Pull Request, trước khi merge vào nhánh chính.
Cụ thể: khi một analyst mở PR sửa model dư nợ, CI tự động build model đó trên môi trường test/dữ liệu mẫu và chạy toàn bộ test. PR chỉ được merge khi test xanh. Nhờ vậy lỗi bị chặn ở tay người viết, trước khi chạm production. Đây là mắt xích nối thẳng sang CI/CD ở bài 5 — nơi ta tự động hoá đúng vòng lặp này.
Đối soát và control totals
Ngoài các assertion đơn lẻ, ngân hàng cần lớp kiểm cao hơn: đối soát (reconciliation) và control totals.
- Reconciliation — so hai nguồn độc lập cho ra cùng con số. Ví dụ: tổng số dư tài khoản trong data warehouse phải khớp tổng từ hệ thống sổ cái (general ledger). Lệch một đồng là có vấn đề. Chủ đề này gắn chặt với gov-03 chất lượng dữ liệu.
- Control totals — chốt kiểm bằng con số tổng: tổng số bản ghi, tổng tiền, số khách hàng, đem so giữa các tầng pipeline. Nếu staging có 1.000.000 giao dịch mà marts chỉ còn 990.000, đã mất 10.000 dòng ở đâu đó — control total bắt ngay.
Minh hoạ một control total dạng đối soát nội bộ (kiểm phân bố dòng theo tiền tệ, dùng để so với kỳ trước):
-- ▶ Chạy được
SELECT currency,
COUNT(*) AS so_tai_khoan,
SUM(balance) AS tong_so_du
FROM accounts
GROUP BY currency
ORDER BY tong_so_du DESC;
Data test ≠ data observability
Đây là ranh giới hay bị lẫn, cần phân định rõ (đào sâu ở bài 6 observability và obs-01):
| Data test | Data observability | |
|---|---|---|
| Bản chất | Kỳ vọng bạn biết trước | Phát hiện bất thường chưa biết |
| Cách hoạt động | Assertion tường minh (pass/fail) | Học baseline, cảnh báo khi lệch |
| Ví dụ | "balance >= 0" | "Số dòng hôm nay bất thường so với 30 ngày qua" |
| Trả lời | "Điều tôi lường trước có xảy ra không?" | "Có gì lạ mà tôi chưa nghĩ tới không?" |
Test bắt cái bạn đã nghĩ tới; observability bắt cái bạn chưa nghĩ tới. Cả hai bổ sung nhau, không thay thế. Một đội trưởng thành có cả bộ test tường minh và lớp observability giám sát nền.
Bổ trợ cho cả hai là data contract — thoả thuận chính thức giữa bên sản xuất và bên tiêu thụ dữ liệu về schema, ngữ nghĩa, chất lượng và SLA. Contract biến kỳ vọng ngầm thành cam kết có thể kiểm được: nhiều data test chính là hiện thực hoá các điều khoản trong contract.
Xử lý khi test FAIL
Test đỏ rồi làm gì? Không phải mọi lỗi đều nên chặn cứng. Có ba mức xử lý:
- Warn (cảnh báo, cho qua). Với lỗi nhẹ hoặc test còn đang hiệu chỉnh — ghi cảnh báo nhưng để pipeline chạy tiếp. Trong dbt: đặt
severity: warn. - Block / Error (chặn cứng). Với bất biến sống-còn (referential integrity, khoá trùng, số dư âm) — dừng pipeline, không cho dữ liệu xấu đi tiếp. Đây là hành vi circuit breaker.
- Cách ly (quarantine). Thay vì bỏ toàn bộ lô, tách riêng các bản ghi xấu sang một bảng cách ly để điều tra, cho phần còn lại đi tiếp. Hữu ích khi một vài dòng bẩn không nên chặn cả triệu dòng sạch.
Đi kèm luôn phải là cảnh báo (alerting) — gửi thông báo tới kênh vận hành (Slack/Teams/email/PagerDuty) kèm ngữ cảnh: test nào fail, bao nhiêu dòng vi phạm, ở model nào. Test đỏ mà không ai biết thì vô nghĩa.
Use case thực tế
Bối cảnh. Đội dữ liệu NCB có báo cáo phân loại nợ theo nhóm 1–5 gửi hằng tháng cho khối tuân thủ, sau đó tổng hợp vào báo cáo gửi NHNN. Quý trước xảy ra sự cố: một tháng nọ, hệ core banking đổi cách ghi mã chi nhánh, khiến ~3% giao dịch có account_id không khớp bảng tài khoản. Các giao dịch "mồ côi" này rơi khỏi tổng dư nợ, làm số nợ nhóm 4–5 báo thấp hơn thực tế. Lỗi lọt tới bàn tuân thủ mới bị phát hiện, phải giải trình và chạy lại.
Triển khai bộ test sau sự cố:
| Chốt | Test | Mức | Con số baseline |
|---|---|---|---|
| Đầu vào | Source freshness giao dịch < 24h | Block | — |
| Đầu vào | Schema staging đủ cột, đúng kiểu | Block | — |
| Sau transform | Referential: transactions.account_id khớp accounts.id | Block | 0 dòng mồ côi |
| Sau transform | Unique account_no; not-null khoá | Block | 0 trùng |
| Sau transform | Range: balance >= 0 (trừ tài khoản tín dụng) | Warn | — |
| Trước khi phục vụ | Control total: tổng dư nợ marts = tổng từ sổ cái | Block | Lệch ≤ 0,01% |
| Trước khi phục vụ | Volume: số giao dịch/ngày trong ±40% trung bình 30 ngày | Warn | ~1,2 triệu/ngày |
Kết quả sau 3 tháng. Test referential (dựng đúng như truy vấn LEFT JOIN ở trên) chạy trên mỗi lần build; ngay tháng đầu nó bắt được 18.400 giao dịch mồ côi và chặn pipeline trước khi số liệu vào báo cáo. Cảnh báo bắn về kênh vận hành trong 4 phút, đội xử lý mapping chi nhánh trong ngày. Con số sai không bao giờ chạm tới báo cáo tuân thủ. Đây chính là giá trị của test đặt đúng vị trí, đúng mức chặn.
Ghi nhớ
- Kiểm thử là trái tim DataOps: bắt lỗi sớm (fail fast) và cho phép thay đổi tự tin (change with confidence).
- Phân biệt hai loại test: test CODE/logic (unit test hàm biến đổi, như phần mềm) và test DỮ LIỆU (data test — kiểm dữ liệu thật có đúng kỳ vọng vì dữ liệu đổi ngoài kiểm soát). Cần cả hai.
- Các data test cốt lõi: schema, not-null, unique, accepted values, referential, range, freshness, volume, phân phối. Về bản chất mỗi test là một SELECT tìm bản ghi vi phạm.
- Công cụ: dbt tests (generic + singular, xem dbt-04), Great Expectations, Soda.
- Test theo VỊ TRÍ: source freshness (đầu vào) → sau transform → trước khi phục vụ. Đây là các circuit breaker chặn dữ liệu xấu lan xuống.
- Shift-left: chạy test trên PR/CI trước khi merge — dẫn sang bài 5 CI/CD.
- Reconciliation & control totals đối soát giữa các nguồn/tầng, đặc biệt quan trọng với báo cáo tuân thủ ngân hàng (xem gov-03).
- Data test ≠ observability: test là kỳ vọng biết trước; observability bắt bất thường chưa biết (bài 6). Data contract biến kỳ vọng ngầm thành cam kết kiểm được.
- Xử lý khi FAIL: warn / block / quarantine + luôn kèm cảnh báo. Bất biến sống-còn thì chặn cứng.
Bài viết liên quan
CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.