Product Management
Đăng nhập
ESC

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

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

Data-Driven Testing

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

Hãy tưởng tượng bạn là tester ở một sàn thương mại điện tử Việt Nam như Tiki. Sếp giao cho bạn kiểm thử ô nhập mã giảm giá ở trang thanh toán. Nghe thì đơn giản, nhưng khi ngồi liệt kê ra, bạn sẽ thấy có hàng chục trường hợp: mã hợp lệ còn hạn, mã hết hạn, mã đã dùng, mã viết hoa/viết thường, mã có khoảng trắng thừa, mã áp dụng cho danh mục khác, mã yêu cầu đơn tối thiểu 200.000đ, mã trống, mã có ký tự đặc biệt... Nếu bạn viết mỗi trường hợp thành một hàm test riêng, bạn sẽ có 20-30 hàm gần như giống hệt nhau, chỉ khác đúng cái mã và kết quả mong đợi. Sao chép, dán, sửa — và mỗi lần logic thay đổi, bạn phải sửa 30 chỗ.

Đây chính là bài toán mà Data-Driven Testing (kiểm thử theo dữ liệu, viết tắt DDT) sinh ra để giải quyết. Ý tưởng cốt lõi ngắn gọn: một logic test duy nhất, chạy đi chạy lại với nhiều bộ dữ liệu khác nhau. Bạn tách phần "làm gì" (điền mã, bấm áp dụng, kiểm tra kết quả) ra khỏi phần "làm với dữ liệu nào" (mã X kỳ vọng kết quả Y). Nhờ đó, muốn thêm một trường hợp kiểm thử, bạn chỉ cần thêm một dòng dữ liệu, không đụng vào code.

Bài này quan trọng vì DDT là một trong những kỹ thuật nền tảng của mọi framework automation trưởng thành. Dù bạn dùng Selenium, Cypress hay chỉ test API, bạn đều sẽ gặp lại pattern này. Nắm vững nó, bạn nhân độ phủ kiểm thử lên nhiều lần mà gần như không tốn thêm công viết code — đúng tinh thần "làm ít, phủ nhiều" mà một kỹ sư kiểm thử giỏi luôn theo đuổi.

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

Data-Driven Testing là gì?

Data-Driven Testing là kỹ thuật thiết kế test trong đó dữ liệu đầu vào và kết quả mong đợi được lưu trữ tách biệt khỏi mã kịch bản test. Kịch bản (test script) chỉ chứa logic thao tác, còn dữ liệu nằm ở một nguồn bên ngoài: file CSV, Excel, JSON, cơ sở dữ liệu, hoặc một mảng ngay trong code. Framework sẽ lặp qua từng dòng dữ liệu, nạp vào cùng một logic và chạy — mỗi dòng trở thành một ca kiểm thử (test case) độc lập.

Hãy so sánh hai cách viết cùng một bài toán "kiểm tra hàm cộng":

// Cách cũ: mỗi ca một hàm, lặp lại logic
@Test void testCong_2_va_3() { assertEquals(5, tinh.cong(2, 3)); }
@Test void testCong_am() { assertEquals(-1, tinh.cong(2, -3)); }
@Test void testCong_khong() { assertEquals(0, tinh.cong(0, 0)); }
// Cách Data-Driven với JUnit 5: một logic, nhiều bộ data
@ParameterizedTest
@CsvSource({
    "2, 3, 5",
    "2, -3, -1",
    "0, 0, 0"
})
void testCong(int a, int b, int ketQua) {
    assertEquals(ketQua, tinh.cong(a, b));
}

Cả hai chạy đúng 3 ca, nhưng cách thứ hai chỉ có một khối logic. Muốn thêm ca "100, 200, 300", bạn thêm một dòng vào @CsvSource. Đó là toàn bộ tinh thần của DDT.

Ba thành phần luôn có

Một thiết kế DDT tốt luôn tách bạch ba phần:

  • Nguồn dữ liệu (data source): nơi chứa các bộ input + expected output. Có thể là CSV, JSON, Excel, DB, hay bảng inline.
  • Trình đọc dữ liệu (data provider/reader): đoạn code đọc nguồn và biến mỗi dòng thành một bộ tham số. Trong JUnit 5 là các annotation @CsvSource, @MethodSource, @CsvFileSource; trong pytest là @pytest.mark.parametrize; trong TestNG là @DataProvider.
  • Logic test (test script): phần thao tác thực sự, nhận tham số và kiểm tra. Phần này viết đúng một lần.

Phân biệt với Keyword-Driven và Parameterization

Đừng nhầm DDT với vài khái niệm họ hàng (những khái niệm này có bài riêng, ở đây tôi chỉ vạch ranh giới để bạn không lẫn):

  • Data-Driven thay đổi dữ liệu, giữ nguyên các bước.
  • Keyword-Driven (Bài 11) thay đổi cả chuỗi hành động được mô tả bằng "từ khóa" trong bảng — linh hoạt hơn nhưng phức tạp hơn.
  • Parameterization đơn thuần chỉ là truyền tham số vào hàm; DDT là ứng dụng có hệ thống của nó cho mục đích test, thường kèm nguồn dữ liệu ngoài.

Vì sao DDT giúp "tester non-coder"?

Đây là lợi ích ít người nói tới nhưng cực giá trị ở môi trường Việt Nam, nơi nhiều đội QA có manual tester chưa mạnh về code. Khi dữ liệu nằm trong file Excel/CSV, một tester nghiệp vụ giỏi — người hiểu rõ 50 trường hợp mã giảm giá cần kiểm — có thể tự thêm hàng vào file mà không cần biết Java hay Python. Automation engineer chỉ cần dựng khung một lần. Đây là cách chia việc rất hiệu quả: người hiểu nghiệp vụ lo dữ liệu, người code lo logic.

Tình huống thực tế

Tình huống 1 — Fintech VN kiểm thử tính lãi khoản vay

Một công ty fintech ở TP.HCM (gọi là VayNhanh) có một hàm tính lãi trả góp. Ban đầu, đội QA viết 12 hàm test riêng cho 12 gói vay (5 triệu/6 tháng, 10 triệu/12 tháng, 50 triệu/24 tháng...). Mỗi lần bộ phận tài chính điều chỉnh công thức làm tròn, tester phải sửa cả 12 hàm — mất nửa ngày và hai lần để sót một hàm chưa cập nhật, gây ra bug lọt lên staging.

Sau khi chuyển sang DDT, họ đưa toàn bộ 12 gói (và bổ sung dần lên 40 gói) vào một file goi_vay.csv với các cột: soTien, soThang, laiSuat, tongPhaiTra. Logic test rút gọn còn đúng một hàm parameterized. Bài học: khi con số ca kiểm thử tăng nhưng các bước không đổi, DDT không chỉ tiết kiệm code mà còn loại bỏ rủi ro "sửa chỗ này quên chỗ kia". Bộ phận tài chính — vốn không biết code — được giao quyền tự thêm gói vay mới vào CSV, giảm tải cho team automation.

Tình huống 2 — Grab và bài toán validate số điện thoại đa quốc gia

Một đội QA làm về đăng ký tài khoản cho một siêu ứng dụng gọi xe hoạt động khắp Đông Nam Á phải kiểm thử ô nhập số điện thoại. Số hợp lệ ở Việt Nam (+84), Thái Lan (+66), Singapore (+65), Indonesia (+62) có độ dài và tiền tố khác nhau; cộng thêm các ca lỗi: thiếu số, thừa số, có chữ cái, có dấu cách, số bắt đầu bằng 0 sau mã vùng...

Họ dựng một file JSON gần 60 dòng, mỗi dòng là { "input": "...", "quocGia": "...", "hopLe": true/false }. Một test parameterized chạy cả 60 ca trong vài giây. Điểm hay: khi Grab mở rộng sang Myanmar (+95), tester nghiệp vụ chỉ thêm khoảng 8 dòng JSON cho định dạng số Myanmar, không cần automation engineer động tay. Bài học: DDT đặc biệt tỏa sáng ở các bài toán kiểm thử biên (boundary) và validation đầu vào, nơi số lượng biến thể dữ liệu rất lớn nhưng logic kiểm tra giống nhau.

Tình huống 3 — Cái bẫy khi lạm dụng DDT

Một bạn junior ở Đà Nẵng, sau khi mê DDT, quyết định gộp mọi thứ vào một file Excel khổng lồ 300 dòng cho luồng đặt hàng end-to-end: đăng nhập, chọn sản phẩm, thanh toán, mỗi bước lấy dữ liệu từ Excel. Kết quả: khi một dòng lỗi, log chỉ báo "row 147 failed" mà không rõ thất bại ở bước nào; file Excel trở thành "hộp đen" khó bảo trì; thời gian chạy tăng vọt vì mỗi dòng đều làm lại toàn bộ luồng nặng nề.

Bài học ngược: DDT hợp nhất với các bài toán cùng một logic gọn, nhiều biến thể dữ liệu. Nó không phải chiếc búa cho mọi cái đinh. Với luồng nghiệp vụ dài nhiều bước và ít biến thể, các pattern khác (như Page Object Model ở Bài 9, hoặc tách nhỏ kịch bản) phù hợp hơn. Đừng nhồi 300 dòng vào một test chỉ vì bạn "có thể".

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

Tôi sẽ hướng dẫn bạn xây một test data-driven hoàn chỉnh cho ví dụ kiểm thử validate email, dùng Python + pytest (dễ đọc cho người mới). Nguyên tắc áp dụng được cho mọi ngôn ngữ.

Bước 1 — Xác định logic lặp lại. Hãy hỏi: "Các ca kiểm thử của mình có chung bao nhiêu bước?" Nếu chung gần hết, chỉ khác dữ liệu, thì DDT là lựa chọn đúng. Ở đây logic là: đưa một chuỗi email vào hàm la_email_hop_le(), kiểm tra kết quả đúng như mong đợi.

Bước 2 — Thiết kế bộ dữ liệu. Liệt kê các ca: email chuẩn, thiếu @, thiếu tên miền, có dấu cách, chuỗi rỗng, email hợp lệ dạng phức tạp. Với mỗi ca ghi rõ input và expected. Tôi khuyên bạn thêm một cột "mô tả" để log dễ đọc — đây là mẹo tách biệt tình huống 3 ở trên khỏi rắc rối.

Bước 3 — Chọn nguồn dữ liệu. Ít ca (dưới ~15) và chỉ dev dùng thì để inline trong code. Nhiều ca hoặc muốn tester nghiệp vụ tham gia thì tách ra file CSV/JSON. Ta bắt đầu với inline cho gọn:

import pytest
from validator import la_email_hop_le

@pytest.mark.parametrize("email, mong_doi, mo_ta", [ ("an@vietcos.com", True, "email chuan"), ("an.nguyen@fpt.edu.vn", True, "email nhieu dau cham"), ("an@", False, "thieu ten mien"), ("@vietcos.com", False, "thieu ten nguoi dung"), ("an vietcos.com", False, "thieu ky tu @"), ("", False, "chuoi rong"), (" an@vietcos.com ", False, "co khoang trang thua"), ]) def test_email(email, mong_doi, mo_ta): assert la_email_hop_le(email) == mong_doi, f"That bai o ca: {mo_ta}"

Bước 4 — Chạy và đọc kết quả. Chạy pytest -v, bạn sẽ thấy pytest liệt kê 7 test riêng biệt, mỗi cái mang tên ca. Đây là điểm mấu chốt: mỗi dòng dữ liệu trở thành một test độc lập, khi một cái fail thì các cái khác vẫn chạy, và log chỉ đích danh ca lỗi.

Bước 5 — Nâng cấp lên nguồn dữ liệu ngoài (khi cần). Khi số ca lên tới hàng chục, chuyển sang file. Tạo emails.csv:

email,mong_doi,mo_ta
an@vietcos.com,True,email chuan
an@,False,thieu ten mien

Rồi đọc file trong fixture và truyền vào parametrize. Lúc này tester nghiệp vụ chỉ cần mở CSV thêm dòng, không đụng code Python.

Bước 6 — Tích hợp expected output động. Với các bài phức tạp (như tính lãi ở tình huống 1), đôi khi expected không cố định mà tính từ input. Hãy để cột expected trong dữ liệu là kết quả đã được kiểm định thủ công (golden value), tránh viết lại đúng công thức đang test — nếu công thức sai, cả code lẫn expected sai giống nhau thì test vô nghĩa.

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

Lỗi 1 — Nhét cả expected output "tính bằng logic đang test". Đây là lỗi tinh vi nhất. Nếu bạn tính mong_doi bằng chính hàm đang kiểm thử, test luôn xanh dù hàm sai. Luôn dùng giá trị kỳ vọng độc lập, xác định bằng tay hoặc từ đặc tả nghiệp vụ.

Lỗi 2 — Quên cột mô tả, log khó đọc. Khi test fail với thông báo "case 23 failed", bạn sẽ mất thời gian dò xem dòng 23 là gì. Luôn thêm nhãn/mô tả cho mỗi bộ dữ liệu và đưa vào thông điệp assert hoặc ids (pytest có tham số ids=, JUnit có @DisplayName động).

Lỗi 3 — Dữ liệu chung khả biến giữa các ca. Nếu các ca dùng chung một object/list bị sửa đổi (mutable), ca sau có thể "kế thừa" trạng thái bẩn từ ca trước, gây flaky. Hãy đảm bảo mỗi ca dùng dữ liệu độc lập, khởi tạo lại trong fixture.

Lỗi 4 — Nhồi luồng end-to-end dài vào DDT. Như tình huống 3, DDT hợp với logic gọn nhiều biến thể, không hợp luồng dài. Nếu thấy file dữ liệu có quá nhiều cột điều khiển "bước nào chạy, bước nào bỏ", đó là dấu hiệu bạn đang lấn sang Keyword-Driven — hãy cân nhắc lại.

Lỗi 5 — Encoding tiếng Việt trong file dữ liệu. File CSV/Excel chứa dữ liệu có dấu tiếng Việt hay bị lỗi font nếu không lưu UTF-8. Luôn đọc file với encoding="utf-8" và lưu file ở định dạng UTF-8, đặc biệt khi test các trường tên người Việt.

Mẹo hay:

  • Bắt đầu inline, chỉ tách file khi vượt ~15 ca hoặc cần cho non-coder.
  • Đặt các ca "biên" và "âm" (invalid) nhiều hơn ca "thuận" — bug thường nấp ở biên.
  • Đưa mỗi bộ dữ liệu một id rõ nghĩa để báo cáo (Bài 27 về Allure) đẹp và dễ truy vết.
  • Đừng để một file dữ liệu phình quá 50-100 dòng; nếu cần, tách theo nhóm chức năng.

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

  • Cơ bản: Viết một hàm xep_loai_diem(diem) trả về "Giỏi/Khá/Trung bình/Yếu" theo thang điểm Việt Nam (>=8 Giỏi, >=6.5 Khá, >=5 Trung bình, còn lại Yếu). Dùng @pytest.mark.parametrize (hoặc @CsvSource nếu bạn dùng Java) tạo tối thiểu 8 ca, bao gồm các giá trị biên: 8.0, 7.9, 6.5, 5.0, 4.9, 10, 0.
  • Trung cấp: Chuyển bộ dữ liệu bài 1 ra file diem.csv với cột diem,ket_qua,mo_ta. Viết đoạn code đọc CSV (nhớ encoding="utf-8") và nạp vào test. So sánh trải nghiệm bảo trì so với inline.
  • Nâng cao & phản biện: Cho luồng "đăng ký tài khoản Momo giả định" gồm 5 bước. Hãy phân tích: phần nào của luồng phù hợp DDT (ví dụ validate số điện thoại, mật khẩu), phần nào KHÔNG nên nhét vào DDT. Viết một đoạn ngắn (5-7 dòng) lý giải quyết định của bạn — đây là kỹ năng thiết kế quan trọng hơn cả việc viết code.

Tóm tắt

Data-Driven Testing là kỹ thuật chạy một logic test duy nhất với nhiều bộ dữ liệu, bằng cách tách dữ liệu (input + expected) khỏi mã kịch bản. Lợi ích chính: nhân độ phủ kiểm thử lên nhiều lần mà gần như không viết thêm code, giảm mạnh lỗi "sửa chỗ này quên chỗ kia", và cho phép tester nghiệp vụ không biết code tự đóng góp dữ liệu qua file CSV/Excel/JSON.

Một thiết kế DDT tốt luôn có ba phần rạch ròi: nguồn dữ liệu, trình đọc dữ liệu, và logic test viết một lần. Bắt đầu với dữ liệu inline khi ít ca, tách ra file khi nhiều ca hoặc cần cho non-coder. Ba tình huống — fintech tính lãi, validate số điện thoại đa quốc gia, và cái bẫy nhồi luồng dài — cho thấy DDT tỏa sáng ở bài toán validation nhiều biến thể, nhưng không phải chiếc búa vạn năng. Tránh các lỗi kinh điển: tính expected bằng chính logic đang test, quên nhãn mô tả, dùng chung dữ liệu khả biến, và lỗi encoding tiếng Việt.

Nắm vững DDT, bạn đã có một công cụ nền tảng sẽ đi cùng mình qua Selenium, API, unit test và mọi framework sau này. Ở bài tiếp theo, chúng ta sẽ bước lên một tầng linh hoạt hơn: Keyword-Driven Testing — nơi không chỉ dữ liệu mà cả chuỗi hành động cũng được đưa ra ngoài code.

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