Product Management
Đăng nhập
ESC

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

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

JMeter — Variables, CSV Data Set, Extractors

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

Hãy tưởng tượng bạn vừa dựng xong một Test Plan trong JMeter để kiểm thử API đăng nhập của một sàn thương mại điện tử. Bạn hardcode thẳng URL https://api-staging.shop.vn/v2/login, hardcode luôn email nguyenvana@gmail.com và mật khẩu 123456 vào Sampler. Chạy 1 luồng thì đẹp. Nhưng khi sếp bảo "chạy thử 500 người dùng cùng lúc" thì thảm hoạ bắt đầu: 500 luồng cùng đăng nhập bằng một tài khoản duy nhất. Server có cơ chế khoá tài khoản khi đăng nhập bất thường, thế là 499 request trả về lỗi 429, và báo cáo hiệu năng của bạn trở nên vô nghĩa — bạn đang đo tốc độ của cơ chế chống spam chứ không phải hiệu năng thật.

Đây chính là lý do bài học hôm nay tồn tại. Một bài kiểm thử hiệu năng nghiêm túc gần như không bao giờ dùng dữ liệu tĩnh. Người dùng thật đăng nhập bằng hàng nghìn tài khoản khác nhau, xem hàng nghìn sản phẩm khác nhau, và mỗi request thường phụ thuộc vào kết quả của request trước đó (ví dụ: lấy token từ bước login để dùng cho bước đặt hàng). Ba công cụ giúp bạn làm được điều này trong JMeter là: Variables (biến để tái sử dụng và cấu hình linh hoạt), CSV Data Set (nạp dữ liệu thật từ file bên ngoài), và Extractors (trích xuất giá trị động từ response để dùng lại). Nắm vững ba thứ này, bạn chuyển từ "người bấm nút chạy tool" thành "kỹ sư thiết kế kịch bản tải thực tế".

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

Cú pháp biến trong JMeter

Trước hết, bạn cần phân biệt hai cú pháp mà JMeter dùng — nhầm lẫn giữa chúng là lỗi kinh điển của người mới:

  • ${ten_bien} — tham chiếu tới một biến (variable). Biến được đánh giá lại mỗi lần luồng (thread) đi qua, và mỗi luồng có bản sao riêng.
  • ${__hamGiaTri()} — gọi một function dựng sẵn của JMeter, ví dụ ${__Random(1,1000)}, ${__threadNum}, ${__time()}. Dấu gạch dưới kép __ là dấu hiệu nhận biết.
Biến trong JMeter mặc định là cục bộ theo từng luồng. Nghĩa là nếu Thread 1 đặt biến token = ABC, thì Thread 2 hoàn toàn không thấy ABC đó — nó có token riêng. Điều này rất quan trọng và cũng rất hợp lý: mỗi người dùng ảo phải có phiên đăng nhập riêng.

User Defined Variables (UDV)

UDV là nơi bạn khai báo các giá trị cấu hình tĩnh, dùng chung cho cả Test Plan. Bạn thêm nó qua chuột phải vào Test Plan hoặc Thread Group → Add → Config Element → User Defined Variables. Ví dụ điển hình:

base_url    = https://api-staging.shop.vn
api_version = v2
timeout_ms  = 5000

Sau đó trong HTTP Request Sampler, thay vì gõ URL đầy đủ, bạn viết Server Name là ${base_url} (bỏ phần https:// ra config riêng) hoặc dùng path là /${api_version}/products. Giá trị thực tế sẽ là /v2/products.

Lợi ích cực lớn: khi cần chuyển từ môi trường staging sang production, bạn chỉ sửa một dòng base_url thay vì đi sửa 50 Sampler. UDV được đánh giá một lần khi bắt đầu test và giữ nguyên suốt phiên chạy — nên nó không phù hợp cho dữ liệu cần thay đổi liên tục.

Một biến thể mạnh hơn là Config Element → CSV Data Set Config cho dữ liệu động, và User Parameters (nằm trong Pre Processor) khi bạn cần mỗi luồng nhận một bộ giá trị khác nhau theo vòng lặp.

CSV Data Set Config — nạp dữ liệu thật

Đây là công cụ để "bơm" dữ liệu thật vào test. Bạn chuẩn bị một file .csv như sau:

email,password
user001@test.vn,Pass@001
user002@test.vn,Pass@002
user003@test.vn,Pass@003

Thêm Config Element → CSV Data Set Config, khai báo:

  • Filename: đường dẫn tới file (nên dùng đường dẫn tương đối so với nơi chạy .jmx để dễ mang lên server).
  • Variable Names: email,password (nếu file có header, bạn có thể để trống và tick "First line is header").
  • Delimiter: dấu phân cách, mặc định là dấu phẩy.
  • Recycle on EOF: True nghĩa là đọc hết file thì quay lại dòng đầu (dùng khi cần chạy liên tục lâu).
  • Stop thread on EOF: True nếu muốn dừng luồng khi hết dữ liệu (dùng khi mỗi tài khoản chỉ được dùng đúng một lần).
  • Sharing mode: All threads (tất cả luồng cùng đọc chung, mỗi luồng lấy dòng kế tiếp — đây là chế độ hay dùng nhất).
Sau khi cấu hình, trong Sampler bạn dùng ${email}${password}. Mỗi lần một luồng chạy qua, nó tự lấy dòng tiếp theo trong file. Đây chính là cách bạn phân phối 5.000 tài khoản thật cho 500 luồng chạy 10 vòng.

Extractors — trích xuất giá trị động từ response

Extractor giải quyết bài toán phụ thuộc: bước sau cần giá trị do bước trước sinh ra. Ba loại phổ biến nhất, đặt dưới dạng Post Processor của Sampler:

  • JSON Extractor (còn gọi JSON JMESPath/JSONPath): dùng khi response là JSON. Ví dụ response {"data":{"token":"eyJhbG..."}}, bạn dùng JSONPath $.data.token để lưu vào biến access_token.
  • Regular Expression Extractor: dùng regex để bắt giá trị trong bất kỳ text nào (HTML, header, JSON thô). Linh hoạt nhất nhưng dễ viết sai.
  • Boundary Extractor: bắt giá trị nằm giữa hai chuỗi mốc (left boundary và right boundary) — dễ đọc hơn regex, phù hợp khi HTML thay đổi thường xuyên.
Điểm mấu chốt: sau khi extractor lưu giá trị vào biến (ví dụ access_token), bạn dùng nó ở Sampler tiếp theo qua ${access_token} — thường đặt vào HTTP Header Manager dưới dạng Authorization: Bearer ${access_token}.

Tình huống thực tế

Tình huống 1 — Tiki và bài toán "một tài khoản chạy 300 luồng"

Một nhóm QA làm cho một sàn TMĐT lớn (giả định theo mô hình Tiki) được giao kiểm thử API giỏ hàng chịu 300 người dùng đồng thời. Ban đầu bạn junior tester hardcode một tài khoản test duy nhất và một product_id = 12345 cố định. Kết quả: throughput đo được chỉ 40 req/s, latency p95 lên tới 4 giây — báo động đỏ.

Khi điều tra, họ phát hiện product_id = 12345 là một sản phẩm cụ thể, và mọi request đều đọc/ghi vào cùng một dòng tồn kho trong database. Đây là hiện tượng row lock contention — không phản ánh tải thật, vì người dùng thật xem hàng nghìn sản phẩm khác nhau. Họ sửa lại bằng cách tạo file products.csv chứa 2.000 product_id thật lấy từ database staging, thêm CSV Data Set Config với Sharing mode = All threads, và dùng ${product_id}. Chạy lại: throughput vọt lên 380 req/s, p95 chỉ còn 600ms.

Bài học: Dữ liệu tĩnh không chỉ làm sai kết quả, nó còn tạo ra một nút thắt cổ chai giả mà production không hề gặp. CSV Data Set là công cụ bắt buộc để mô phỏng sự đa dạng của dữ liệu thật.

Tình huống 2 — VNPAY và chuỗi token phụ thuộc

Một team fintech kiểm thử luồng thanh toán qua cổng (mô hình VNPAY). Luồng gồm 3 bước: (1) đăng nhập lấy access_token, (2) tạo giao dịch để nhận transaction_idotp_ref, (3) xác nhận thanh toán bằng transaction_id. Rõ ràng bước 3 không thể hardcode transaction_id vì mỗi giao dịch sinh ra một ID khác nhau, ngẫu nhiên theo thời gian thực.

Giải pháp: sau Sampler bước 1, họ gắn JSON Extractor với JSONPath $.data.access_token lưu vào biến access_token. Sau Sampler bước 2, họ gắn JSON Extractor $.data.transaction_id lưu vào txn_id. Bước 3 dùng body {"transaction_id":"${txn_id}"} kèm header Authorization: Bearer ${access_token}. Nhờ chuỗi extractor này, mỗi luồng ảo chạy một giao dịch độc lập, đúng như người dùng thật.

Có một chi tiết họ suýt bỏ sót: khi giao dịch thất bại, response không chứa transaction_id, JMeter gán cho biến giá trị mặc định. Nếu không đặt "Default Value" là một chuỗi dễ nhận biết như TXN_NOT_FOUND, bước 3 sẽ gửi ${txn_id} nguyên văn (chuỗi literal) và làm nhiễu số liệu lỗi.

Bài học: Extractor là xương sống của kịch bản nhiều bước. Luôn đặt Default Value có ý nghĩa để phát hiện khi trích xuất thất bại, thay vì để lỗi âm thầm trôi qua.

Tình huống 3 — Shopee flash sale và Recycle on EOF

Một team chuẩn bị test đợt sale 12.12 với yêu cầu chạy soak test 2 giờ liên tục, tải ổn định. Họ có file users.csv với 10.000 tài khoản. Ban đầu họ để Recycle on EOF = FalseStop thread on EOF = True. Kết quả: sau khoảng 20 phút, số luồng active tụt dần về 0 vì file hết dữ liệu, và biểu đồ tải sụp đổ giữa chừng — báo cáo trông như hệ thống chết dù thực ra chỉ hết tài khoản.

Họ chỉnh Recycle on EOF = True: khi đọc hết 10.000 dòng, JMeter tự quay lại dòng đầu. Test chạy đủ 2 giờ với tải ổn định. Tuy nhiên họ lưu ý: recycle nghĩa là cùng một tài khoản sẽ đăng nhập lại nhiều lần — chấp nhận được cho soak test, nhưng KHÔNG chấp nhận được nếu bài test yêu cầu mỗi tài khoản dùng đúng một lần (ví dụ test mã giảm giá dùng một lần).

Bài học: Recycle và Stop-on-EOF là hai công tắc quyết định hành vi của cả bài test. Chọn sai không làm test crash, nhưng làm bạn hiểu sai về kết quả.

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

Ta sẽ dựng một kịch bản hoàn chỉnh: đăng nhập bằng tài khoản từ CSV, lấy token, rồi gọi API xem thông tin cá nhân.

  • Tạo file dữ liệu accounts.csv đặt cạnh file .jmx:
   email,password
   user001@test.vn,Pass@001
   user002@test.vn,Pass@002
   
  • Khai báo cấu hình chung: Chuột phải Test Plan → Add → Config Element → User Defined Variables. Thêm base_url = api-staging.shop.vnapi_version = v2.
  • Thêm CSV Data Set: Chuột phải Thread Group → Add → Config Element → CSV Data Set Config. Đặt Filename = accounts.csv, tick "Ignore first line (First line is header)" nếu file có header, Variable Names = email,password, Sharing mode = All threads.
  • Sampler đăng nhập: Thêm HTTP Request. Server Name = ${base_url}, Protocol = https, Method = POST, Path = /${api_version}/login. Trong tab Body Data ghi:
   {"email":"${email}","password":"${password}"}
   
Đừng quên thêm HTTP Header Manager với Content-Type: application/json.

  • Gắn Extractor lấy token: Chuột phải Sampler login → Add → Post Processor → JSON Extractor. Names = access_token, JSON Path expressions = $.data.token, Match No. = 1, Default Values = TOKEN_NOT_FOUND.
  • Sampler thứ hai dùng token: Thêm HTTP Request mới. Path = /${api_version}/me, Method = GET. Thêm HTTP Header Manager với Authorization: Bearer ${access_token}.
  • Kiểm chứng bằng Debug Sampler: Thêm một Debug Sampler và một View Results Tree Listener. Chạy 1 luồng, mở Debug Sampler trong kết quả — bạn sẽ thấy giá trị thực tế của email, password, access_token. Đây là cách xác nhận biến đã được gán đúng trước khi tăng tải.
  • Tăng tải dần: Sau khi 1 luồng chạy đúng, mới nâng Thread Group lên 50, 100, 500. Không bao giờ tăng tải khi kịch bản 1 luồng còn sai.

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

  • Nhầm ${bien}${__ham}: ${Random} không tồn tại; phải là ${__Random(1,100)}. Thiếu dấu gạch dưới kép là JMeter coi đó như một biến chưa định nghĩa và trả về nguyên văn chuỗi ${Random}.
  • CSV không đọc được do đường dẫn: Khi chạy Non-GUI trên server Linux, đường dẫn tuyệt đối kiểu Windows C:\data\... sẽ hỏng. Luôn dùng đường dẫn tương đối và đặt CSV cạnh file .jmx. Mẹo: kiểm tra log JMeter, dòng File ... not found xuất hiện ngay khi sai đường dẫn.
  • Encoding tiếng Việt bị lỗi: File CSV chứa tên tiếng Việt có dấu phải lưu ở UTF-8, và trong CSV Data Set Config đặt File Encoding = UTF-8. Bỏ qua bước này, Nguyễn Văn A biến thành ký tự rác.
  • Extractor không khớp trả về Default: Nếu ${access_token} luôn ra TOKEN_NOT_FOUND, hãy mở View Results Tree xem response thật. Thường là do JSONPath sai cấp ($.token trong khi thực tế là $.data.token), hoặc response bị bọc thêm một lớp.
  • Biến bị chia sẻ ngoài ý muốn: Nhớ rằng biến JMeter cục bộ theo luồng. Nếu bạn thực sự cần một giá trị dùng chung toàn cục giữa các luồng, phải dùng function ${__setProperty} / ${__P} chứ không phải biến thường.
  • Regex tham lam (greedy): Regex (.*) bắt quá nhiều. Ưu tiên dùng Boundary Extractor hoặc JSON Extractor cho JSON — chỉ dùng Regex khi thật sự cần.
  • Mẹo giảm tải cho JMeter: View Results Tree rất ngốn bộ nhớ. Bật nó khi debug, TẮT hẳn khi chạy test tải thật để không làm sai số đo hiệu năng của chính máy chạy test.

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

  • Cơ bản: Tạo file products.csv với 5 product_id. Dựng Sampler GET /${api_version}/products/${product_id} dùng biến base_url từ UDV. Chạy Thread Group 5 luồng và xác nhận qua View Results Tree rằng mỗi luồng gọi một product_id khác nhau.
  • Trung bình: Dựng chuỗi 2 bước — login lấy access_token bằng JSON Extractor, rồi gọi API cần token. Cố tình đặt sai JSONPath, quan sát giá trị Default xuất hiện, rồi sửa lại cho đúng. Ghi lại cách bạn phát hiện lỗi.
  • Nâng cao: Với file 100 tài khoản, cấu hình một lần với Recycle on EOF = True và một lần với Stop thread on EOF = True, chạy 200 luồng. So sánh biểu đồ số luồng active theo thời gian và giải thích vì sao chúng khác nhau. Kết luận: kịch bản nào phù hợp cho soak test, kịch bản nào cho test mã giảm giá dùng một lần?

Tóm tắt

Ba công cụ hôm nay biến JMeter từ một cỗ máy bắn request đơn điệu thành một trình mô phỏng người dùng thật. User Defined Variables cho bạn cấu hình tập trung, sửa một chỗ áp dụng mọi nơi. CSV Data Set Config bơm dữ liệu đa dạng từ file bên ngoài, tránh cái bẫy nút thắt giả do dữ liệu tĩnh — cùng hai công tắc sống còn là Recycle và Stop-on-EOF. Extractors (JSON, Regex, Boundary) trích xuất giá trị động từ response, tạo nên các kịch bản nhiều bước phụ thuộc nhau như login → giao dịch → xác nhận. Nguyên tắc vàng xuyên suốt: luôn debug đúng ở 1 luồng bằng Debug Sampler và View Results Tree trước khi tăng tải, và luôn đặt Default Value có ý nghĩa để lỗi trích xuất không âm thầm làm hỏng cả báo cáo. Khi bạn kiểm soát được ba thứ này, bạn đã có nền tảng để dựng bất kỳ kịch bản tải thực tế nào ở các bài sau.

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