Product Management
Đăng nhập
ESC

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

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

Performance assertions in Postman

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

Có một cái bẫy mà rất nhiều bạn QA mới vào nghề mắc phải: nghĩ rằng "API trả về đúng dữ liệu là xong việc". Nhưng khách hàng thật không nghĩ vậy. Một API /api/products trả về đúng 20 sản phẩm mà mất 4,2 giây thì với người dùng nó vẫn là một app "chậm rùa", và họ sẽ thoát ra trước cả khi màn hình load xong.

Trong toàn bộ khóa học này, chúng ta đã và sẽ đi qua rất nhiều dạng kiểm thử: schema validation, chained requests, authorization, data-driven... Tất cả đều xoay quanh câu hỏi "API trả về đúng cái gì?". Bài 49 hôm nay đặt một câu hỏi khác, thường bị bỏ quên trong functional test: "API trả về nhanh tới mức nào?".

Cần nói thẳng ngay từ đầu để bạn không hiểu lầm: Postman không phải là công cụ load test. Nó không thay thế JMeter hay k6 (nội dung này thuộc Bài 30 và Bài 56, tôi sẽ không lấn sân). Postman chạy request tuần tự, một luồng, nên nó không mô phỏng được 5.000 người dùng đồng thời. Nhưng — và đây là điểm cốt lõi của bài — trong mỗi functional test bạn viết, response time luôn có sẵn đó, miễn phí, chỉ chờ bạn assert nó. Bỏ qua nó là bỏ phí một tín hiệu cảnh báo cực kỳ rẻ. Một suite functional test có gắn ngưỡng response time hợp lý sẽ bắt được hồi quy hiệu năng (performance regression) ngay từ commit đầu tiên, thay vì đợi tới lúc production sập.

Bài này dạy bạn cách biến response time — một con số đang trôi qua vô ích — thành một cửa kiểm soát chất lượng thực sự trong bộ test của mình.

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

pm.response.responseTime — con số bạn cần

Sau mỗi request, Postman sandbox cho bạn truy cập pm.response.responseTime — thời gian tính bằng mili-giây (ms) kể từ lúc gửi request đến lúc nhận đủ response. Đây là nền tảng của mọi performance assertion trong Postman.

pm.test("Response time dưới 500ms", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

Chỉ ba dòng, nhưng nó vừa biến một API "trả đúng dữ liệu" thành một API "trả đúng dữ liệu đủ nhanh". Nếu response time vượt 500ms, test này fail — và nó fail ngay trong pipeline CI/CD, trước khi tới tay người dùng.

Chọn ngưỡng (threshold) như thế nào cho đúng?

Đây là phần khó nhất và cũng là nơi phân biệt QA nghiệp dư với QA chuyên nghiệp. Con số 500ms ở trên không phải chân lý. Ngưỡng phải xuất phát từ loại endpointkỳ vọng thực tế, không phải copy đại một con số trên mạng.

Một khung tham chiếu thực dụng tôi thường dùng:

  • Health check / ping (/health, /status): dưới 100–200ms. Đây là endpoint siêu nhẹ, chậm hơn thế là dấu hiệu hạ tầng có vấn đề.
  • Đọc đơn giản (lấy 1 bản ghi theo ID, GET /users/123): dưới 300–500ms.
  • Truy vấn có join, lọc, phân trang (GET /orders?status=pending): dưới 800ms–1s.
  • Ghi / xử lý nghiệp vụ (tạo đơn hàng, thanh toán): 1–2s có thể chấp nhận, tùy nghiệp vụ.
  • Báo cáo / tổng hợp nặng: có thể vài giây, nhưng nên có ngưỡng riêng.
Nguyên tắc vàng: đo trước, đặt ngưỡng sau. Chạy endpoint vài chục lần trong môi trường ổn định, xem con số dao động ở đâu, rồi đặt ngưỡng cao hơn giá trị p95 (95% các lần chạy) một khoảng đệm. Đặt ngưỡng bằng đúng thời gian trung bình sẽ khiến test "chớp tắt" (flaky) — lúc pass lúc fail vô cớ.

Percentile quan trọng hơn trung bình

Nếu bạn chỉ nhớ một ý trong phần khái niệm, hãy nhớ điều này: trung bình (average) là kẻ nói dối. Giả sử 100 request có 95 request chạy 200ms và 5 request chạy 3.000ms. Trung bình là 340ms — nghe rất đẹp. Nhưng 5% người dùng của bạn đang chịu 3 giây khủng khiếp. Đó là lý do dân hiệu năng luôn nói bằng ngôn ngữ percentile: p50 (median), p95, p99.

Postman không tự tính percentile cho bạn trong một request đơn lẻ, nhưng khi chạy qua Collection Runner hoặc Newman với nhiều vòng lặp, bạn có thể tự gom responseTime vào một biến environment rồi tính. Tôi sẽ chỉ cách ở phần Hướng dẫn.

Các loại assertion hiệu năng bạn có thể viết

Ngoài ngưỡng đơn giản below, sandbox của Chai.js cho phép diễn đạt linh hoạt:

// Trong một khoảng
pm.expect(pm.response.responseTime).to.be.within(50, 800);

// Kết hợp với header caching để kiểm tra hiệu quả cache pm.test("Response được cache và nhanh", function () { pm.expect(pm.response.responseTime).to.be.below(150); pm.expect(pm.response.headers.get("X-Cache")).to.eql("HIT"); });

// Cảnh báo mềm: không fail nhưng ghi log if (pm.response.responseTime > 1000) { console.warn(⚠ Chậm bất thường: ${pm.response.responseTime}ms); }

Lưu ý phân biệt fail cứng (dùng pm.test — làm đỏ pipeline) và cảnh báo mềm (dùng console.warn — chỉ để mắt tới). Không phải mọi độ trễ đều đáng làm gãy build.

Tình huống thực tế

Tình huống 1 — Tiki và cú hồi quy vì thiếu index

Hãy tưởng tượng đội QA của một sàn thương mại điện tử lớn như Tiki đang kiểm thử API GET /api/v2/products/search. Trong suite regression, một bạn QA đã thêm dòng assert responseTime < 700ms từ nhiều tháng trước — endpoint search vốn chạy ổn định quanh 400ms.

Một sprint nọ, đội backend refactor lại truy vấn tìm kiếm, vô tình bỏ quên một database index trên cột category_id. Về mặt chức năng, mọi thứ hoàn hảo: API vẫn trả đúng danh sách sản phẩm, status 200, schema đúng chằn chặn. Toàn bộ functional assertion về nội dung đều xanh.

Nhưng test responseTime < 700ms chuyển đỏ. Con số nhảy lên 1.850ms vì truy vấn giờ phải full-table-scan trên hàng triệu dòng. Nhờ dòng assert nhỏ xíu đó, pipeline CI báo đỏ ngay trên pull request, backend nhận ra thiếu index và fix trong vòng một giờ — trước khi merge.

Bài học: performance assertion đóng vai trò như một "cầu chì" cho hồi quy hiệu năng. Nó không nói cho bạn biết tại sao chậm, nhưng nó hét lên rằng có gì đó đã chậm đi — và đó thường là đủ để chặn thảm họa. Không có nó, lỗi này sẽ chỉ lộ ra khi người dùng thật phàn nàn.

Tình huống 2 — Fintech VN và bẫy "cold start" gây flaky test

Một startup fintech ở TP.HCM triển khai backend trên kiến trúc serverless (giả định dùng AWS Lambda). Đội QA gắn assert responseTime < 500ms cho endpoint POST /api/transfer/validate. Kết quả: test cứ chạy trên CI vào buổi sáng sớm là fail, còn chạy ban ngày lại pass. Rất khó chịu, không ai hiểu vì sao.

Nguyên nhân là cold start: request đầu tiên vào một Lambda "nguội" phải mất thời gian khởi tạo container, dễ vọt lên 1.200ms; các request sau đó, khi Lambda đã "ấm", chỉ còn 180ms. Sáng sớm CI chạy trước tiên nên hứng trọn cú cold start.

Đội QA xử lý theo hai bước. Thứ nhất, thêm một request "làm nóng" (warm-up) chạy trước trong collection và không assert response time của nó. Thứ hai, với các endpoint serverless, họ nới ngưỡng lên < 800ms và bổ sung một assert mềm ghi log khi vượt 500ms để vẫn theo dõi được xu hướng.

Bài học: ngưỡng hiệu năng phải hiểu bối cảnh hạ tầng. Một con số cứng nhắc trong môi trường serverless, hay môi trường staging dùng chung tài nguyên yếu, sẽ khiến test flaky và dần dần cả đội mất niềm tin vào test — điều còn tệ hơn cả không có test.

Tình huống 3 — Đo p95 cho API giỏ hàng bằng Newman

Một team ở Shopee (giả định bối cảnh) muốn nhiều hơn một con số đơn lẻ cho API GET /cart. Họ chạy request này 100 vòng bằng Newman trong CI đêm, gom toàn bộ responseTime lại và tính p95 ngay trong script. Kết quả cho thấy p50 là 210ms rất đẹp, nhưng p95 lên tới 1.600ms — nghĩa là cứ 20 lần mở giỏ hàng thì có 1 lần người dùng chờ hơn 1,5 giây.

Con số trung bình (khoảng 340ms) đã che giấu hoàn toàn cái đuôi dài này. Nhờ đo p95, đội phát hiện vấn đề nằm ở connection pool cạn kiệt khi có nhiều request đồng thời, và tối ưu lại. (Cách tính p95 trong Newman/Runner tôi trình bày ngay dưới đây.)

Bài học: với endpoint quan trọng, đừng dừng ở một con số. Chạy nhiều vòng và nhìn vào percentile để thấy được trải nghiệm của nhóm người dùng "kém may mắn nhất".

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

Bước 1 — Thêm assertion cơ bản vào tab Tests. Mở request cần kiểm, vào tab Tests, thêm:

pm.test("Response time nằm trong ngưỡng cho phép", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

Bước 2 — Đưa ngưỡng vào biến environment thay vì hard-code. Đừng rải con số 500 khắp nơi. Tạo biến max_response_time trong environment (xem lại Bài 5 và Bài 6 về environments và variables) để mỗi môi trường có ngưỡng riêng — staging chậm hơn production là bình thường.

const threshold = parseInt(pm.environment.get("max_response_time")) || 500;
pm.test(Response time dưới ${threshold}ms, function () {
    pm.expect(pm.response.responseTime).to.be.below(threshold);
});

Bước 3 — Phân loại theo endpoint. Nếu dùng chung một collection cho nhiều loại endpoint, đặt ngưỡng theo tag/folder. Ví dụ dùng một biến collection perf_read = 500perf_write = 1500, request nào GET thì đọc perf_read, request POST/PUT thì đọc perf_write.

Bước 4 — Thu thập số liệu qua nhiều vòng để tính percentile. Trong tab Tests, gom mỗi lần chạy vào một mảng lưu ở environment:

let samples = JSON.parse(pm.environment.get("rt_samples") || "[]");
samples.push(pm.response.responseTime);
pm.environment.set("rt_samples", JSON.stringify(samples));

Bước 5 — Tính p95 ở request cuối cùng (hoặc một request tổng kết riêng).

let samples = JSON.parse(pm.environment.get("rt_samples") || "[]");
samples.sort((a, b) => a - b);
const p95 = samples[Math.ceil(samples.length * 0.95) - 1];
console.log(Số mẫu: ${samples.length}, p95 = ${p95}ms);

pm.test("p95 dưới 1000ms", function () { pm.expect(p95).to.be.below(1000); }); pm.environment.unset("rt_samples"); // dọn dẹp cho lần chạy sau

Bước 6 — Chạy qua Collection Runner hoặc Newman với số vòng lặp. Đặt 50–100 iterations trong Runner, hoặc newman run collection.json -n 100. Với nhiều vòng lặp, con số percentile mới có ý nghĩa thống kê. (Newman thuộc Bài 13; ở đây ta chỉ tận dụng nó làm nơi chạy nhiều vòng.)

Bước 7 — Gắn vào CI/CD. Vì assertion nằm ngay trong test, khi Newman chạy trong pipeline (Bài 16–17), bất kỳ vi phạm ngưỡng nào cũng khiến exit code khác 0 và làm đỏ build — không cần thêm công cụ gì.

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

Lỗi 1 — Đặt ngưỡng bằng đúng thời gian trung bình. Đây là nguyên nhân số một gây flaky test. Nếu endpoint trung bình 480ms mà bạn đặt < 500ms, chỉ cần mạng rung nhẹ là fail. Hãy đặt ngưỡng dựa trên p95 quan sát được cộng biên độ an toàn 20–50%.

Lỗi 2 — Nhầm Postman là công cụ load test. Xin nhắc lại: Postman/Newman chạy tuần tự một luồng. Response time bạn đo là độ trễ khi không có tải. Nó không nói lên hệ thống chịu được bao nhiêu người dùng đồng thời. Câu hỏi tải đồng thời thuộc về k6/JMeter (Bài 30, 56). Đừng bao giờ báo cáo "API chịu được 1.000 user" dựa trên Postman.

Lỗi 3 — Bỏ qua cold start và request đầu tiên. Như tình huống fintech, hãy thêm một warm-up request không-assert, hoặc loại bỏ mẫu đầu tiên khi tính percentile.

Lỗi 4 — So sánh táo với cam giữa các môi trường. Response time trên máy dev cắm dây LAN cạnh server sẽ khác xa CI runner ở data center khác châu lục. Đừng lấy ngưỡng của môi trường này áp cho môi trường kia. Dùng biến environment như Bước 2.

Lỗi 5 — Quên rằng độ trễ mạng của chính bạn cũng nằm trong con số. responseTime bao gồm cả thời gian đi trên đường truyền tới server. Nếu bạn test API đặt ở Singapore từ mạng nhà ở Việt Nam vào giờ cao điểm, một phần độ chậm là do mạng của bạn, không phải server. Khi cần con số "sạch", hãy chạy test từ CI runner gần server (cùng khu vực) để loại bớt nhiễu.

Mẹo hay: Ghi kèm response time vào tên test động (pm.test(\... (${pm.response.responseTime}ms)\)) để khi đọc báo cáo Newman/HTML (Bài 14) bạn thấy ngay con số thực tế mà không cần mở console.

Mẹo hay 2: Kết hợp performance assertion với kiểm tra cache header như ví dụ ở phần khái niệm — đây là cách rẻ nhất để phát hiện cache bị hỏng, vì cache hỏng thường biểu hiện qua response time tăng đột biến trước cả khi có lỗi chức năng.

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

  • Cơ bản. Chọn một API công khai bất kỳ (ví dụ https://jsonplaceholder.typicode.com/posts). Viết một test assert responseTime dưới ngưỡng bạn tự chọn. Chạy 10 lần, quan sát con số dao động, rồi điều chỉnh ngưỡng cho hợp lý (cao hơn giá trị lớn nhất quan sát được khoảng 30%).
  • Biến environment. Tạo hai environment localstaging với max_response_time khác nhau (ví dụ 300 và 800). Viết test đọc ngưỡng từ biến. Xác nhận cùng một request cho kết quả pass/fail khác nhau tùy environment đang chọn.
  • Percentile. Dùng Collection Runner chạy một request 50 vòng, gom responseTime vào biến, rồi tính và in ra p50, p95, p99. So sánh p95 với giá trị trung bình — bạn thấy khoảng cách bao nhiêu?
  • Phân biệt cứng/mềm. Viết một request có cả assert cứng (pm.test với ngưỡng 1.000ms) và một cảnh báo mềm (console.warn khi vượt 500ms). Giải thích khi nào bạn dùng loại nào trong thực tế.
  • Suy ngẫm (không code). Với một API thanh toán như VNPay sandbox (mà bạn sẽ gặp ở Bài 36), bạn sẽ đặt ngưỡng response time bao nhiêu và vì sao? Việc thanh toán chậm 3 giây có phải luôn là "lỗi" không, hay đôi khi là chấp nhận được? Viết vài dòng lập luận.

Tóm tắt

Postman không phải là công cụ load test, nhưng mỗi functional test bạn viết đều có sẵn pm.response.responseTime — một tín hiệu hiệu năng miễn phí mà bỏ qua thì quá phí. Bằng cách thêm chỉ vài dòng assert vào tab Tests, bạn biến response time thành một cầu chì bắt hồi quy hiệu năng ngay trong pipeline CI/CD, trước khi người dùng thật gánh chịu.

Những điểm cần khắc cốt ghi tâm: (1) chọn ngưỡng theo loại endpointđo trước, đặt sau, đừng copy con số bừa; (2) trung bình là kẻ nói dối — với endpoint quan trọng hãy chạy nhiều vòng và nhìn p95/p99; (3) đưa ngưỡng vào biến environment để mỗi môi trường có kỳ vọng riêng; (4) cẩn thận với cold start, độ trễ mạng của chính bạn, và đừng đặt ngưỡng sát quá kẻo test flaky; và (5) phân biệt fail cứng với cảnh báo mềm — không phải mọi độ trễ đều đáng làm gãy build.

Ba câu chuyện về Tiki thiếu index, fintech dính cold start, và giỏ hàng có p95 xấu đều cho thấy cùng một điều: một dòng assert response time nhỏ bé, đặt đúng chỗ, có thể là ranh giới giữa việc bắt lỗi trên pull request và việc để khách hàng phát hiện giúp bạn. Ở Bài 56, bạn sẽ dùng đúng tư duy này nhưng với công cụ tải thật sự để trả lời câu hỏi "hệ thống chịu được bao nhiêu người" — còn ở đây, hãy làm chủ việc gác cổng độ trễ trong chính bộ functional test của mình.

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