Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là QA tại một sàn thương mại điện tử ở TP.HCM. Đội dev vừa refactor lại API POST /login. Bạn cần kiểm tra: đăng nhập đúng mật khẩu thì trả 200, sai mật khẩu trả 401, tài khoản bị khóa trả 403, email không tồn tại trả 404, và mật khẩu để trống trả 400. Nếu làm theo cách cũ, bạn sẽ nhân bản (duplicate) cùng một request thành năm lần, mỗi lần sửa tay body và sửa tay assertion. Vừa mất thời gian, vừa dễ sai, và khi API thay đổi bạn phải sửa cả năm chỗ.
Đây chính là bài toán mà Data-driven Testing (kiểm thử theo dữ liệu) giải quyết. Ý tưởng cốt lõi rất gọn: bạn viết một request duy nhất, giữ nguyên logic assertion, rồi để Postman chạy đi chạy lại request đó với nhiều bộ dữ liệu khác nhau lấy từ một file bên ngoài (CSV hoặc JSON). Mỗi dòng dữ liệu là một "kịch bản" độc lập.
Với QA chuyên nghiệp, kỹ thuật này không phải "cho vui" mà là công cụ chủ lực để phát hiện regression trên diện rộng. Chỉ với một collection nhỏ chạy 50 dòng dữ liệu, bạn quét được 50 tổ hợp đầu vào chỉ trong vài giây — điều mà kiểm thử thủ công không bao giờ theo kịp về tốc độ lẫn độ phủ. Đây cũng là nền tảng để sau này bạn tự động hóa hoàn toàn với Newman và CI/CD. Nắm chắc bài này, bạn chuyển từ tư duy "test từng cái một" sang tư duy "test cả một bảng đầu vào cùng lúc" — đúng chất kỹ sư kiểm thử.
Khái niệm cốt lõi
Iteration là gì?
Trong Postman, khi bạn chạy một collection bằng Collection Runner, bạn có thể chỉ định số lần lặp (iterations). Nếu bạn gắn một data file vào lần chạy đó, Postman sẽ tự đặt số iteration bằng số dòng dữ liệu, và ở mỗi vòng lặp nó nạp một dòng vào một tập biến đặc biệt gọi là data variables.
Nói cách khác: mỗi dòng trong file dữ liệu = một iteration = một lần request được gửi đi với bộ giá trị riêng.
Data variables và cách gọi
Giá trị từ file dữ liệu được truy cập bằng cú pháp biến quen thuộc {{tenCot}} trong phần request (URL, header, body), và bằng pm.iterationData.get("tenCot") trong script (pre-request hoặc test).
Ví dụ, nếu file có cột username, bạn dùng {{username}} trong body JSON, và pm.iterationData.get("username") khi cần so sánh trong test.
Điểm quan trọng về scope (phạm vi): data variables chỉ tồn tại trong đúng iteration hiện tại. Sang dòng tiếp theo, chúng bị ghi đè bằng giá trị dòng mới. Chúng cũng có độ ưu tiên rất cao — cao hơn environment và collection variable, nên nếu trùng tên, giá trị từ data file sẽ thắng.
CSV vs JSON — chọn định dạng nào?
Postman hỗ trợ hai định dạng file dữ liệu:
CSV — dòng đầu tiên là header (tên cột), mỗi dòng sau là một iteration. Định dạng phẳng, dễ đọc, dễ tạo từ Excel/Google Sheets. Phù hợp khi dữ liệu đơn giản, mỗi trường là một giá trị nguyên thủy (chuỗi, số).
test-data.csv:
username,password,expected_status
admin@shop.vn,Admin@123,200
user@shop.vn,saipass,401
locked@shop.vn,Locked@1,403
ghost@shop.vn,anything,404
,,400
JSON — là một mảng các object. Phù hợp khi dữ liệu có cấu trúc lồng nhau (nested), ví dụ một đơn hàng có mảng sản phẩm bên trong, hoặc khi bạn cần truyền cả một object làm biến.
test-data.json:
[
{
"username": "admin@shop.vn",
"password": "Admin@123",
"expected_status": 200,
"profile": { "role": "admin", "region": "HCM" }
},
{
"username": "user@shop.vn",
"password": "saipass",
"expected_status": 401,
"profile": { "role": "customer", "region": "HN" }
}
]
Quy tắc thực dụng: dữ liệu phẳng → dùng CSV; dữ liệu lồng nhau hoặc cần kiểu dữ liệu chính xác (số, boolean, object) → dùng JSON. Lưu ý CSV mặc định đọc mọi thứ thành chuỗi, nên 200 từ CSV thực chất là "200" — điều này gây bẫy khi so sánh, ta sẽ nói kỹ ở phần lỗi thường gặp.
Assertion động theo dữ liệu
Sức mạnh thật sự nằm ở chỗ: assertion của bạn cũng "biến hóa" theo dữ liệu. Thay vì hardcode pm.response.to.have.status(200), bạn viết:
const expected = Number(pm.iterationData.get("expected_status"));
pm.test(Status phải là ${expected}, function () {
pm.expect(pm.response.code).to.eql(expected);
});
Giờ mỗi dòng dữ liệu kiểm tra chính status code mà nó kỳ vọng. Một request, một script, năm kịch bản khác nhau.
Tình huống thực tế
Ví dụ 1 — Tiki: kiểm thử ma trận đăng nhập trước ngày sale
Bối cảnh: Đội QA của một sàn giả định giống Tiki chuẩn bị cho đợt sale 12/12. API auth được viết lại để chịu tải cao hơn. QA lead yêu cầu kiểm tra kỹ mọi nhánh trả về của POST /api/v2/login vì đây là cửa ngõ — nếu login lỗi, cả sàn "đứng hình".
Bạn tạo một file login-matrix.csv với 30 dòng, phủ đủ các trường hợp: mật khẩu đúng, sai, tài khoản khóa, email chưa xác thực, email sai định dạng, mật khẩu quá ngắn, chuỗi SQL injection thử ' OR '1'='1, và email có ký tự tiếng Việt như nguyễn@shop.vn.
Chỉ một request duy nhất trong collection, gắn file, bấm Run. Sau 4 giây, Runner báo: 28/30 iteration pass, 2 fail. Hai dòng fail chính là dòng email chưa xác thực (dev trả 200 thay vì 403) và dòng SQL injection (server trả 500 thay vì 400). Bạn chụp màn hình, log bug, gắn kèm chính xác dòng dữ liệu gây lỗi.
Bài học: Data-driven biến một danh sách test case trên giấy thành một file chạy được. Khi dev sửa xong, bạn chỉ cần chạy lại đúng file đó — có ngay hồi quy (regression) mà không dựng lại gì.
Ví dụ 2 — Fintech Đông Nam Á: validate phí giao dịch theo hạng khách hàng
Bối cảnh: Một startup ví điện tử ở Singapore phục vụ cả thị trường Việt Nam có API POST /transactions/fee tính phí theo hạng thành viên (basic, plus, premium) và theo số tiền. Business rule khá rối: dưới 100.000đ miễn phí cho plus trở lên, basic luôn tính 0,5%, premium miễn phí đến 5 triệu.
QA dùng file JSON vì mỗi case cần cả object customer lồng bên trong lẫn giá trị expected_fee là số thực:
[
{ "tier": "basic", "amount": 200000, "expected_fee": 1000 },
{ "tier": "plus", "amount": 80000, "expected_fee": 0 },
{ "tier": "premium", "amount": 4000000, "expected_fee": 0 },
{ "tier": "premium", "amount": 6000000, "expected_fee": 12000 }
]
Test script so sánh expected_fee với phí thật server trả. Chạy 40 dòng phủ mọi ngưỡng, đội QA phát hiện đúng một bug tại biên: khi amount bằng đúng 5.000.000đ cho premium, server tính phí thay vì miễn — sai lệch ở dấu > thay vì >= trong code. Đây là loại bug biên (boundary) rất khó bắt bằng test thủ công vì ai cũng test 4 triệu và 6 triệu, hiếm ai test đúng 5 triệu.
Bài học: Data-driven cực mạnh cho boundary testing. Bạn liệt kê các giá trị biên vào file, để máy chạy hết. JSON giúp giữ đúng kiểu số để phép so sánh không sai lệch.
Ví dụ 3 — Baemin/Grab-style: tạo dữ liệu ngẫu nhiên kết hợp file cố định
Bối cảnh: Một team QA của app giao đồ ăn cần test POST /orders với nhiều nhà hàng và món ăn khác nhau (lấy từ CSV), nhưng mỗi đơn phải có mã đơn duy nhất để tránh trùng khi chạy lặp lại.
Họ dùng CSV cho phần cố định (restaurant_id, item_id, quantity) và dùng pre-request script để sinh phần động:
pm.variables.set("orderId", "ORD-" + Date.now() + "-" + pm.info.iteration);
pm.info.iteration trả về chỉ số vòng lặp hiện tại (bắt đầu từ 0), kết hợp timestamp đảm bảo mã không trùng. Body request trộn cả hai nguồn: {{restaurant_id}} từ file và {{orderId}} từ script.
Bài học: File dữ liệu không phải giới hạn duy nhất — bạn có thể kết hợp dữ liệu tĩnh từ file với dữ liệu động sinh trong script để tạo kịch bản thực tế hơn.
Hướng dẫn từng bước
Bước 1 — Chuẩn bị request "khuôn". Tạo một request duy nhất, thay mọi giá trị thay đổi bằng biến. Ví dụ body:
{ "username": "{{username}}", "password": "{{password}}" }
Bước 2 — Viết test dùng data variable. Trong tab Tests của request:
const expected = Number(pm.iterationData.get("expected_status"));
pm.test(Login case → mong đợi ${expected}, function () {
pm.expect(pm.response.code).to.eql(expected);
});
Bước 3 — Tạo file dữ liệu. Lưu test-data.csv với header khớp đúng tên biến bạn dùng (username,password,expected_status). Chú ý header không dấu cách thừa.
Bước 4 — Mở Collection Runner. Chọn collection (hoặc folder) chứa request, bấm Run.
Bước 5 — Gắn data file. Ở màn hình cấu hình Runner, kéo file CSV/JSON vào ô Select File. Postman sẽ tự nhận số dòng và điền vào ô Iterations. Bấm Preview để xem trước bảng dữ liệu — bước này giúp phát hiện sớm file bị lệch cột.
Bước 6 — Chạy và đọc kết quả. Bấm Run. Mỗi iteration hiện thành một khối riêng, ghi rõ pass/fail và giá trị dữ liệu của dòng đó. Dòng fail dễ dàng truy ra chính xác đầu vào nào gây lỗi.
Bước 7 — Lưu và tái sử dụng. Commit file dữ liệu vào repo cùng collection (export dạng JSON). Sau này chạy lại bằng Newman với cờ --iteration-data test-data.csv để đưa vào pipeline tự động.
Lỗi thường gặp & mẹo
Bẫy kiểu dữ liệu với CSV. Đây là lỗi số một. CSV đọc 200 thành chuỗi "200". Nếu bạn viết pm.expect(pm.response.code).to.eql(pm.iterationData.get("expected_status")) thì so sánh số 200 với chuỗi "200" → luôn fail. Luôn ép kiểu: Number(pm.iterationData.get("expected_status")). Với JSON thì không gặp vì kiểu được giữ nguyên.
Tên cột không khớp tên biến. Nếu header là user_name mà bạn gọi {{username}}, biến sẽ rỗng và request gửi đi giá trị trống. Luôn dùng Preview để đối chiếu, và giữ tên cột nhất quán, không dấu, không khoảng trắng thừa.
File CSV lưu sai encoding. File chứa tiếng Việt (Nguyễn Văn A) mà lưu không phải UTF-8 sẽ ra ký tự lỗi khi Postman đọc. Luôn lưu UTF-8 (Excel: "CSV UTF-8"; hoặc soạn bằng VSCode). Cẩn thận cả BOM — một số công cụ thêm ký tự ẩn đầu file khiến cột đầu tiên bị hỏng tên.
Dấu phẩy trong dữ liệu. Nếu một trường chứa dấu phẩy (ví dụ địa chỉ 123 Lê Lợi, Q1), phải bọc trong dấu ngoặc kép: "123 Lê Lợi, Q1", nếu không CSV sẽ hiểu nhầm thành hai cột.
Nhầm pm.iterationData với pm.variables. Data từ file nằm trong pm.iterationData, không phải pm.environment hay pm.globals. Gọi sai chỗ sẽ trả undefined.
Mẹo — kiểm thử theo folder. Bạn có thể chạy data-driven cho cả một folder gồm nhiều request (ví dụ login → lấy token → gọi API bảo vệ), mỗi iteration chạy trọn chuỗi với một bộ dữ liệu. Rất mạnh cho kịch bản end-to-end.
Mẹo — không nhồi quá nhiều vào một file. Tách file theo mục đích: login-valid.csv, login-invalid.csv, login-boundary.csv. Dễ bảo trì và dễ đọc kết quả hơn một file 500 dòng lẫn lộn.
Bài tập thực hành
- Tạo ma trận đăng nhập. Viết một request
POSTtới một API login bất kỳ (dùnghttps://reqres.in/api/loginhoặc mock của bạn). Tạo filelogin-data.csvgồm 5 dòng: 2 case hợp lệ (200), 2 case sai (400/401), 1 case thiếu field. Viết test so sánh status code với cộtexpected_status(nhớ épNumber). Chạy Runner, xác nhận đúng số pass/fail.
- Chuyển sang JSON có nested. Chuyển bài 1 sang file JSON, thêm một object
metalồng bên trong mỗi dòng, và in giá trị nested ra console bằngconsole.log(pm.iterationData.get("meta")). Quan sát sự khác biệt kiểu dữ liệu giữa CSV và JSON.
- Thử nghiệm biên. Với một API tính phí/giảm giá giả định, tạo file dữ liệu phủ 3 giá trị: ngay dưới ngưỡng, đúng ngưỡng, ngay trên ngưỡng. Viết assertion động và xem có phát hiện bug biên nào không.
- Kết hợp động + tĩnh. Thêm pre-request script sinh
orderIdduy nhất bằngDate.now()vàpm.info.iteration, dùng chung với dữ liệu từ file. Xác nhận mỗi iteration có mã khác nhau qua console.
Tóm tắt
Data-driven testing biến kiểm thử từ "một request một kịch bản" thành "một request nhiều kịch bản" bằng cách tách dữ liệu ra file ngoài. Điểm cần khắc cốt ghi tâm:
- Mỗi dòng dữ liệu = một iteration; truy cập bằng
{{tenCot}}trong request vàpm.iterationData.get()trong script. - CSV cho dữ liệu phẳng, JSON cho dữ liệu lồng nhau và giữ đúng kiểu.
- Assertion nên động — so sánh với cột
expected_*trong file để một script phủ nhiều kịch bản. - Cảnh giác bẫy kiểu dữ liệu của CSV (luôn ép
Number), encoding UTF-8 cho tiếng Việt, và tên cột khớp tên biến. - Đây là kỹ thuật chủ lực để phát hiện regression diện rộng và là bước đệm trực tiếp để tự động hóa với Newman trong CI/CD.