Mở đầu — vì sao bài này quan trọng
Ở các bài trước, bạn đã biết cách dựng một Thread Group, gửi HTTP Sampler và đọc kết quả qua Listener. Nhưng nếu dừng lại ở đó, bạn mới chỉ có một cái máy "bắn request" mù. Nó bắn được, nhận được phản hồi, nhưng nó không biết phản hồi đó đúng hay sai, nó bắn quá nhanh so với người thật, và nó chạy theo một đường thẳng cứng nhắc không giống hành vi người dùng.
Ba nhóm phần tử trong bài này — Assertions (kiểm tra tính đúng đắn), Timers (điều tiết nhịp độ), và Logic Controllers (điều khiển luồng) — chính là thứ biến một script "bắn cho có" thành một bài test đáng tin cậy và giống thật.
Đây là điểm khác biệt giữa một người mới và một Performance Engineer thực thụ. Người mới nhìn dashboard thấy "throughput 2000 req/s, latency thấp" và tuyên bố hệ thống khỏe. Người có kinh nghiệm hỏi lại: "2000 request đó có bao nhiêu cái thực sự trả về đúng dữ liệu, hay server trả HTTP 200 kèm trang lỗi 'Hệ thống đang bảo trì'?" Không có Assertion, bạn đang đo tốc độ trả rác. Không có Timer, bạn đang tạo một cơn bão giả tạo mà không người dùng thật nào tạo ra. Đó là lý do bài này nằm ở vị trí bản lề của module JMeter.
Khái niệm cốt lõi
Assertions — biến "có phản hồi" thành "phản hồi đúng"
Mặc định, JMeter coi một request là thành công nếu nó nhận được HTTP response code 2xx hoặc 3xx. Vấn đề là rất nhiều lỗi thực tế vẫn trả về mã 200. Một API trả 200 OK với body {"error": "out_of_stock"} vẫn bị JMeter tính là pass. Assertion là công cụ để bạn tự định nghĩa: thế nào mới thực sự là "đúng".
Các loại Assertion quan trọng nhất:
| Loại Assertion | Mục đích | Khi nào dùng |
|---|---|---|
| Response Assertion | Kiểm tra response code, kiểm tra body có/không chứa một chuỗi, khớp regex | Loại phổ biến nhất — kiểm tra text, status code |
| JSON Assertion | Kiểm tra giá trị tại một JSONPath cụ thể (VD $.data.status == "success") | Test REST API trả JSON |
| Duration Assertion | Fail nếu request mất quá X mili-giây | Ép SLA thời gian phản hồi ở mức từng request |
| Size Assertion | Kiểm tra kích thước response (byte) | Phát hiện body rỗng hoặc phình bất thường |
| XPath Assertion | Kiểm tra cấu trúc XML/HTML | API trả XML, hoặc kiểm tra element HTML |
Response Assertion có vài trường quan trọng:
- Field to Test: chọn kiểm tra "Text Response" (body), "Response Code", "Response Message", hay "Response Headers".
- Pattern Matching Rules:
Contains(chứa chuỗi con),Matches(khớp regex toàn phần),Equals(bằng tuyệt đối),Substring. - Not / Or: đảo ngược điều kiện, hoặc gộp nhiều pattern theo logic OR.
Timers — làm cho tải trở nên "giống người"
Nếu không có Timer, mỗi thread (mỗi virtual user) sẽ gửi request tiếp theo ngay lập tức sau khi nhận phản hồi, không nghỉ một mili-giây nào. Không người dùng thật nào hành xử như vậy. Người thật đọc trang, suy nghĩ, di chuột — khoảng nghỉ đó gọi là think time. Timer chính là cách bạn mô phỏng think time.
| Loại Timer | Hành vi |
|---|---|
| Constant Timer | Nghỉ cố định X ms giữa các request |
| Uniform Random Timer | Nghỉ ngẫu nhiên trong khoảng [base, base+random] — giống người hơn |
| Gaussian Random Timer | Nghỉ theo phân phối chuẩn quanh một giá trị trung tâm |
| Constant Throughput Timer | Ép tổng throughput về một mức cố định (VD 300 request/phút) bất kể số thread |
| Precise Throughput Timer | Phiên bản mới, chính xác hơn để đạt throughput mục tiêu |
Thứ nhất, Timer không phải là câu lệnh tuần tự. Nhiều người nghĩ đặt Timer sau Sampler A thì nó nghỉ sau A. Sai. Timer áp dụng cho tất cả Sampler cùng cấp hoặc thấp hơn trong phạm vi của nó, và JMeter thực thi Timer trước khi gửi Sampler. Vị trí trong cây quyết định phạm vi, không quyết định thứ tự nghỉ theo dòng thời gian như bạn tưởng.
Thứ hai, Constant Throughput Timer tính bằng request/phút, không phải request/giây. Rất nhiều người gõ "300" nghĩ là 300 req/s rồi ngơ ngác vì tải quá thấp — thực ra đó là 300 req/phút = 5 req/s.
Logic Controllers — điều khiển luồng chạy
Logic Controller quyết định các Sampler con của nó chạy khi nào, bao nhiêu lần, theo thứ tự gì. Đây là thứ giúp bạn dựng một kịch bản thật thay vì một danh sách request phẳng.
Các Controller hay dùng nhất:
- Transaction Controller: gộp nhiều Sampler thành một "giao dịch" logic và đo tổng thời gian của cả nhóm. Ví dụ "Checkout" gồm 4 request — bạn muốn biết tổng thời gian checkout, không phải từng request lẻ. Nhớ tick "Generate parent sample" để nó xuất một dòng kết quả tổng.
- Loop Controller: lặp các Sampler con N lần trong mỗi vòng thread.
- If Controller: chỉ chạy nhánh con khi điều kiện đúng (VD chỉ thêm vào giỏ nếu
${remaining_stock} > 0). - Once Only Controller: chạy một lần duy nhất mỗi thread — hoàn hảo cho bước login.
- Throughput Controller: cho một tỉ lệ % thread chạy nhánh này (VD 20% người dùng vào trang khuyến mãi).
- Random / Random Order / Interleave Controller: chọn ngẫu nhiên hoặc luân phiên các nhánh con, mô phỏng hành vi đa dạng.
Tình huống thực tế
Ví dụ 1 — Tiki và bài học "throughput đẹp nhưng toàn lỗi"
Một đội QA của một sàn thương mại điện tử tại TP.HCM (gọi là sàn X, mô hình tương tự Tiki) chạy load test cho API tìm kiếm sản phẩm với 500 concurrent user. Dashboard báo throughput 1.800 req/s, error rate 0%, latency trung bình 180ms. Cả đội mừng, gửi báo cáo "hệ thống sẵn sàng cho ngày sale".
Đến ngày sale thật, hệ thống tìm kiếm trả về trang trắng cho hàng loạt người dùng. Điều tra lại, họ phát hiện: khi service search quá tải, nó trả về HTTP 200 kèm body {"results": [], "degraded": true} — một cơ chế "fail gracefully". JMeter thấy 200 nên tính pass, error rate 0% là ảo.
Bài học rút ra: họ thêm một JSON Assertion kiểm tra $.degraded == false và một Response Assertion kiểm tra body không chứa chuỗi "degraded":true. Chạy lại đúng kịch bản 500 user, error rate thực tế nhảy lên 34% ở mức tải đó. Con số 34% mới là sự thật để ra quyết định. Không có Assertion đúng nghiệp vụ, bạn đang test một hệ thống tưởng tượng.
Ví dụ 2 — Ngân hàng số và Constant Throughput Timer
Một ngân hàng số ở Việt Nam cần chứng minh với bộ phận vận hành rằng API chuyển khoản chịu được đúng 600 giao dịch/phút — con số cam kết SLA, không hơn để tránh phá hoại hệ thống thật đang chạy song song trong môi trường staging dùng chung.
Đội test đầu tiên đặt 200 thread không Timer, kết quả throughput vọt lên 3.000 req/phút, làm nghẽn cả DB staging và khiến team khác phàn nàn. Họ chuyển sang Constant Throughput Timer đặt giá trị 600 với tùy chọn "all active threads in current thread group". Giờ dù để 50 hay 200 thread, JMeter tự điều tiết để tổng throughput bám sát 600 req/phút.
Họ còn bọc chuỗi 3 request (khởi tạo giao dịch → xác thực OTP → xác nhận) vào một Transaction Controller tên "Transfer_Flow" với "Generate parent sample" bật lên, để đo tổng thời gian một lần chuyển khoản hoàn chỉnh — chính là con số người dùng cảm nhận, chứ không phải latency từng bước rời rạc.
Bài học rút ra: trong môi trường dùng chung hoặc khi cần test đúng mức SLA cam kết, Constant Throughput Timer là công cụ kiểm soát tải theo mục tiêu nghiệp vụ, không phải theo số thread. Và Transaction Controller cho bạn con số "thời gian trải nghiệm end-to-end" mà sếp thực sự quan tâm.
Ví dụ 3 — Grab-style app và think time thực tế
Một đội test app đặt xe (mô hình tương tự Grab) mô phỏng luồng: mở app → xem bản đồ → chọn điểm đến → xem giá → đặt xe. Ban đầu họ chạy không Timer, 1.000 thread. Kết quả: server 502 la liệt, họ kết luận "hệ thống chỉ chịu được 300 user".
Một mentor xem lại và chỉ ra: 1.000 user không Timer nghĩa là 1.000 người liên tục spam cả 5 bước không nghỉ một giây — điều không bao giờ xảy ra ngoài đời. Người thật mất 3–8 giây nhìn bản đồ, 2–5 giây chọn điểm đến. Họ thêm Uniform Random Timer (base 3.000ms, random 5.000ms) giữa các bước. Chạy lại 1.000 thread: hệ thống chịu tốt, throughput hợp lý, và con số "sức chịu tải" nhảy từ 300 lên hơn 900 user đồng thời.
Bài học rút ra: thiếu think time làm bạn báo cáo sai sức chịu tải theo hướng bi quan — bạn tạo ra một cơn bão không có thật rồi trách hệ thống yếu. Timer không phải để làm chậm test cho vui; nó là điều kiện tiên quyết để con số bạn báo cáo có ý nghĩa với thực tế kinh doanh.
Hướng dẫn từng bước
Ta dựng một luồng "đăng nhập rồi tìm kiếm" hoàn chỉnh với đủ ba nhóm phần tử.
- Tạo Thread Group với 100 user, ramp-up 30 giây (đã học ở bài trước).
- Thêm Once Only Controller cho bước login. Chuột phải Thread Group → Add → Logic Controller → Once Only Controller. Đặt HTTP Sampler "Login" bên trong. Việc này đảm bảo mỗi user chỉ login một lần dù thread lặp nhiều vòng.
- Thêm Transaction Controller tên "Search_Flow", tick "Generate parent sample". Bên trong đặt 2 Sampler: "Search Request" và "Load Results". Giờ bạn sẽ có một dòng kết quả tổng cho cả giao dịch tìm kiếm.
- Thêm Response Assertion vào Sampler "Search Request": Field to Test = Text Response, Pattern Matching = Contains, thêm pattern
"status":"success". Thêm một pattern nữa với ô Not tick và Contains =error, để fail nếu body chứa chữ "error".
- Thêm JSON Assertion vào cùng Sampler: JSONPath
$.data.total_results, tick "Expect null" bỏ, và đặt điều kiện giá trị>= 0hoặc match một pattern hợp lệ, để chắc chắn cấu trúc JSON đúng.
- Thêm Duration Assertion đặt 2.000ms — bất kỳ request search nào chậm hơn 2 giây bị đánh fail, ép SLA ở mức từng request.
- Thêm Uniform Random Timer ngay dưới Transaction Controller: Constant Delay Offset 2.000ms, Random Delay Maximum 3.000ms. Mỗi user nghỉ 2–5 giây trước khi search — mô phỏng think time.
- Thêm If Controller với điều kiện
${__jexl3(${total_results} > 0)}bọc quanh Sampler "Click Product", để chỉ click sản phẩm khi tìm thấy kết quả (giả sử bạn đã tríchtotal_resultsbằng Extractor ở bài trước).
- Thêm Listener (View Results Tree lúc debug, Summary Report lúc chạy thật) và chạy thử ở chế độ GUI với 2–3 thread trước để soi từng Assertion pass/fail, rồi mới scale lên.
Lỗi thường gặp & mẹo
- Chỉ dựa vào response code mặc định. Đây là lỗi số một. HTTP 200 không có nghĩa là nghiệp vụ đúng. Luôn thêm Assertion kiểm tra nội dung body theo nghiệp vụ thật.
- Assertion đặt sai phạm vi gây fail hàng loạt. Một Response Assertion "phải chứa
success" đặt dưới Thread Group sẽ áp cho cả Sampler login (vốn không trảsuccess), làm login fail oan. Đặt Assertion đúng vào Sampler nó thuộc về.
- Assertion quá đắt về CPU. Assertion dùng regex phức tạp trên body lớn sẽ ngốn CPU của chính máy JMeter, làm sai lệch kết quả (bạn đo được latency cao nhưng thủ phạm là máy test, không phải server). Ưu tiên
Contains/Substringthay vìMatchesregex khi có thể.
- Nhầm đơn vị Constant Throughput Timer. Nhắc lại: giá trị là request/phút. Muốn 10 req/s thì gõ 600.
- Đặt Duration Assertion quá gắt trong lúc ramp-up. Giai đoạn ramp-up latency thường cao do cache lạnh; Duration Assertion 500ms có thể fail hàng loạt oan. Cân nhắc bỏ qua giai đoạn warm-up khi phân tích.
- Quên tick "Generate parent sample" trên Transaction Controller. Không tick thì bạn không có dòng tổng cho giao dịch, chỉ thấy các Sampler con lẻ tẻ — mất đi giá trị của việc gộp giao dịch.
- Mẹo debug: dùng View Results Tree với vài thread và bật hiển thị "Assertion result" để thấy chính xác Assertion nào fail và vì sao. Không bao giờ để View Results Tree bật khi chạy tải thật — nó ngốn RAM khủng khiếp.
- Mẹo phạm vi Timer: nếu chỉ muốn think time ở một chỗ, đặt Timer bên trong đúng Controller/Sampler đó. Timer đặt sai chỗ có thể nhân think time lên nhiều lần ngoài ý muốn vì quy tắc phạm vi.
Bài tập thực hành
- Bắt lỗi giả 200: Dựng một HTTP Sampler tới một API JSON bất kỳ (có thể dùng một mock như
dummyjson.com). Thêm JSON Assertion kiểm tra một trường cụ thể phải bằng giá trị mong đợi. Sau đó cố tình sửa JSONPath sang một giá trị sai và quan sát request chuyển từ pass sang fail dù response code vẫn 200.
- Đo giao dịch end-to-end: Bọc 3 Sampler bất kỳ vào một Transaction Controller có "Generate parent sample". Chạy và so sánh thời gian dòng "parent" với tổng thời gian 3 Sampler con. Giải thích vì sao chúng khác nhau.
- Kiểm soát throughput: Đặt 50 thread, thêm Constant Throughput Timer sao cho tổng throughput đúng 120 request/phút. Chạy 3 phút và xác nhận Summary Report cho throughput bám sát ~2 req/s.
- Mô phỏng người thật: Thêm Uniform Random Timer (base 1.000ms, random 2.000ms) vào một luồng 3 bước. Chạy 20 thread có Timer và không Timer, so sánh throughput và error rate. Viết 3 câu giải thích vì sao con số khác nhau và con số nào đáng tin hơn.
- Rẽ nhánh có điều kiện: Dùng If Controller để chỉ chạy một Sampler "thanh toán" khi một biến
${cart_total}lớn hơn 0. Test cả hai trường hợp biến = 0 và biến > 0.
Tóm tắt
Assertions, Timers và Logic Controllers là ba trụ cột biến script JMeter từ "bắn request mù" thành "bài test đáng tin và giống thật":
- Assertions định nghĩa thế nào là đúng — và chính chúng quyết định con số Error rate bạn báo cáo. HTTP 200 không bao giờ là bằng chứng đủ; hãy kiểm tra nội dung theo nghiệp vụ (Response Assertion, JSON Assertion, Duration, Size). Chú ý phạm vi để tránh fail oan và chi phí CPU của regex nặng.
- Timers đưa think time vào để tải trở nên giống người. Thiếu Timer, bạn báo cáo sai sức chịu tải theo hướng bi quan. Nhớ Constant Throughput Timer tính theo request/phút và Timer áp dụng theo phạm vi trong cây, không theo dòng thời gian tuần tự.
- Logic Controllers dựng kịch bản thật: Transaction Controller đo giao dịch end-to-end, Once Only cho login, If cho rẽ nhánh, Throughput Controller cho phân bổ hành vi người dùng.