Product Management
Đăng nhập
ESC

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

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

Error Handling & Conditional Tests

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

Hãy tưởng tượng bạn đang là QA tại một công ty fintech ở Sài Gòn. 3 giờ sáng, hệ thống CI chạy bộ test API tự động trước khi deploy. Một endpoint trả về lỗi 503 vì database đang bảo trì. Nếu test script của bạn viết ẩu, chỉ một dòng pm.response.json() sẽ ném ra JSONError, làm sập toàn bộ run, và tất cả 40 test phía sau bị đánh dấu là "không chạy" thay vì "cần điều tra". Team dev sáng hôm sau nhìn báo cáo đỏ lòm mà không hiểu chuyện gì xảy ra — chỉ vì một test không biết xử lý lỗi.

Đây chính là lý do bài học này tồn tại. Trong các bài trước, bạn đã học cách viết assertion để kiểm tra response (Bài 8) và toàn cảnh pm.* API (Bài 9). Nhưng viết assertion cho "đường đi đẹp" (happy path) mới chỉ là một nửa công việc. Nửa còn lại — thứ phân biệt một QA nghiệp dư với một tester chuyên nghiệp — là xử lý những gì xảy ra khi mọi thứ không như mong đợi: response không phải JSON, một field bị thiếu, server trả 500, hay logic test cần rẽ nhánh tùy theo dữ liệu thực tế.

Bài này tập trung riêng vào hai kỹ năng: error handling (làm cho test script không tự sập khi gặp dữ liệu bất thường) và conditional tests (viết test biết tự điều chỉnh hành vi theo ngữ cảnh). Đây là kỹ năng nền tảng giúp bộ test của bạn bền vững — chạy được hàng nghìn lần trong CI/CD mà không bị "flaky".

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

Vì sao một test script có thể tự sập?

Test script trong Postman chạy trong một môi trường JavaScript (sandbox). Khi một dòng code ném ra exception mà không được bắt lại, toàn bộ khối script đó dừng ngay lập tức. Điều nguy hiểm là: nếu bạn có 5 pm.test() trong một tab Tests, và test thứ 2 gọi pm.response.json() trên một response HTML lỗi, thì test 3, 4, 5 sẽ không bao giờ được đăng ký chạy.

Kết quả là báo cáo của bạn nói dối. Nó không nói "3 test fail", nó chỉ hiện "2 test chạy" và một thông báo lỗi khó hiểu. Với người đọc báo cáo, đây là thảm họa về khả năng chẩn đoán.

Try/catch — lá chắn cơ bản nhất

try/catch là cấu trúc JavaScript để "thử" một đoạn code có thể ném lỗi, và "bắt" lỗi đó để xử lý một cách chủ động thay vì để nó lan ra:

pm.test('Parse response JSON', () => {
    let json;
    try {
        json = pm.response.json();
    } catch (e) {
        // Không ném lỗi ra ngoài — thay vào đó fail test một cách có kiểm soát
        pm.expect.fail(Response không phải JSON hợp lệ: ${e.message});
    }
    pm.expect(json).to.be.an('object');
});

Điểm mấu chốt: khi response không phải JSON, pm.expect.fail() biến sự cố thành một test fail rõ ràng có thông điệp, thay vì một exception làm sập cả khối. Test tiếp theo vẫn chạy bình thường.

Bảo vệ nhiều test cùng lúc — parse một lần, dùng nhiều lần

Một pattern phổ biến là parse JSON một lần ở đầu tab Tests, rồi tất cả assertion sau đó dùng biến đã parse. Nếu parse thất bại, ta chủ động skip các test phụ thuộc:

let body = null;
let isJson = false;
try {
    body = pm.response.json();
    isJson = true;
} catch (e) {
    console.log('Response body không parse được thành JSON:', e.message);
}

pm.test('Status code là 200', () => { pm.response.to.have.status(200); });

// Chỉ chạy các test cấu trúc khi có JSON hợp lệ (isJson ? pm.test : pm.test.skip)('Có field "orderId"', () => { pm.expect(body).to.have.property('orderId'); });

pm.test.skip() là một công cụ đắt giá: nó đánh dấu test là "đã bỏ qua" (chứ không phải pass giả), giúp báo cáo trung thực.

Conditional tests — test biết rẽ nhánh

Không phải mọi API đều trả về cấu trúc cố định. Một endpoint tìm kiếm có thể trả mảng rỗng khi không có kết quả, hoặc mảng có phần tử khi tìm thấy. Test cứng nhắc sẽ fail sai trong một trong hai trường hợp. Conditional test giải quyết bằng cách kiểm tra ngữ cảnh trước, rồi mới quyết định assertion nào áp dụng:

const results = body.data;

if (results.length > 0) { pm.test('Mỗi sản phẩm có giá hợp lệ', () => { results.forEach(p => { pm.expect(p.price).to.be.a('number').and.above(0); }); }); } else { pm.test('Kết quả rỗng trả về đúng cấu trúc', () => { pm.expect(body).to.have.property('message'); }); }

Phân biệt lỗi "được kỳ vọng" và lỗi "bất ngờ"

Một tư duy quan trọng: không phải mọi status code khác 200 đều là bug. Khi bạn test một endpoint xóa với ID không tồn tại, 404 chính là kết quả đúng. Conditional test giúp bạn khẳng định "đúng loại lỗi":

pm.test('Xử lý đúng theo status', () => {
    const status = pm.response.code;
    if (status === 200) {
        pm.expect(body.success).to.be.true;
    } else if (status === 404) {
        pm.expect(body.error).to.include('not found');
    } else {
        pm.expect.fail(Status không mong đợi: ${status});
    }
});

Tình huống thực tế

Ví dụ 1 — Tiki và cái bẫy "response HTML lỗi"

Một team QA của một sàn thương mại điện tử lớn (gọi giả định là dự án kiểu Tiki) chạy bộ regression 120 request mỗi đêm qua Newman. Một đêm, load balancer trả về trang lỗi HTML 502 Bad Gateway cho khoảng 8 request thay vì JSON. Vấn đề: các test script gọi thẳng pm.response.json().data.length.

Kết quả là 8 request đầu tiên gặp lỗi làm sập script, và vì cách tổ chức collection, hàng loạt test phía sau bị bỏ trống. Báo cáo sáng hôm sau hiện "37 test không xác định" — team mất gần 2 giờ debug thủ công chỉ để phát hiện ra là hạ tầng chập chờn, không phải lỗi ứng dụng.

Bài học rút ra: sau sự cố, team bọc mọi lần parse trong try/catch và thêm một assertion đầu tiên kiểm tra Content-Type. Chỉ khi header là application/json thì các test cấu trúc mới chạy:

pm.test('Response là JSON', () => {
    pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');
});

Từ đó, khi hạ tầng lỗi, báo cáo hiện chính xác "8 request nhận HTML thay vì JSON" — chẩn đoán được ngay trong 30 giây thay vì 2 giờ.

Ví dụ 2 — VNPay sandbox và status code "linh hoạt"

Một bạn QA test tích hợp thanh toán qua VNPay sandbox. Endpoint tạo giao dịch đôi khi trả 200 với responseCode: "00" (thành công), đôi khi trả 200 nhưng responseCode: "24" (khách hủy), và khi sandbox quá tải thì trả 429 Too Many Requests. Test ban đầu chỉ assert status 200responseCode === "00" — nên mỗi lần sandbox trả 429, test fail đỏ và bạn ấy tưởng code mình sai.

Sau khi áp dụng conditional test, logic trở nên đúng với thực tế nghiệp vụ:

pm.test('Xử lý phản hồi VNPay', () => {
    if (pm.response.code === 429) {
        pm.expect.fail('Sandbox rate-limited — cần retry, không phải bug ứng dụng');
    }
    pm.response.to.have.status(200);
    const rc = body.vnp_ResponseCode;
    if (rc === '00') {
        pm.expect(body).to.have.property('vnp_TransactionNo');
    } else {
        // Các mã lỗi nghiệp vụ hợp lệ vẫn phải có mô tả
        pm.expect(body).to.have.property('vnp_Message');
    }
});

Bài học rút ra: conditional test giúp phân biệt rõ "lỗi hạ tầng cần retry" với "kết quả nghiệp vụ hợp lệ" và "bug thật". Báo cáo trở nên đáng tin, và bạn không còn hoảng loạn mỗi lần thấy màu đỏ.

Ví dụ 3 — API tuyển dụng và field tùy chọn (optional)

Một startup HR-tech ở Đà Nẵng có API trả hồ sơ ứng viên. Field phoneNumber là tùy chọn — ứng viên có thể không điền. Test ban đầu assert cứng pm.expect(body.phoneNumber).to.be.a('string'), nên với ứng viên không có số điện thoại, test fail dù dữ liệu hoàn toàn hợp lệ.

Cách xử lý đúng là kiểm tra sự tồn tại trước, rồi mới validate kiểu:

pm.test('Số điện thoại (nếu có) đúng định dạng', () => {
    if (body.phoneNumber !== undefined && body.phoneNumber !== null) {
        pm.expect(body.phoneNumber).to.match(/^(0|\+84)\d{9}$/);
    } else {
        pm.expect.hasOwnProperty; // không có phone là chấp nhận được — test pass
    }
});

Bài học rút ra: với field optional, đừng assert kiểu dữ liệu vô điều kiện. Hãy tách "field này bắt buộc phải tồn tại" khỏi "nếu tồn tại thì phải đúng định dạng". Đây là ranh giới tinh tế giữa test đúng và test gây phiền nhiễu bằng false positive.

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

Đây là quy trình xây dựng một tab Tests bền vững cho bất kỳ request nào:

  • Kiểm tra tầng vận chuyển trước. Assert status code và Content-Type là những việc không cần parse body. Chúng luôn an toàn, nên đặt đầu tiên. Nếu response lỗi ở tầng này, các test sau biết cách phản ứng.
  • Parse body một lần, trong try/catch. Gán vào một biến (let body) và một cờ (let isJson). Không bao giờ gọi pm.response.json() rải rác nhiều nơi — mỗi lần gọi là một điểm sập tiềm tàng.
  • Cổng (gate) các test cấu trúc bằng cờ. Dùng (isJson ? pm.test : pm.test.skip) để các assertion phụ thuộc body chỉ chạy khi có JSON hợp lệ, và được đánh dấu "skip" trung thực khi không.
  • Áp dụng conditional cho logic nghiệp vụ. Khi API có nhiều nhánh phản hồi hợp lệ (thành công / rỗng / lỗi nghiệp vụ), dùng if/else để chọn đúng assertion cho từng nhánh, và luôn có nhánh else cuối cùng với pm.expect.fail() cho những status thực sự bất ngờ.
  • Viết thông điệp lỗi giàu ngữ cảnh. Đừng để pm.expect.fail() trống. Nhét pm.response.code, một phần body, hay e.message vào thông điệp. Người đọc báo cáo lúc 3 giờ sáng sẽ cảm ơn bạn.
  • Dùng console.log cho thông tin chẩn đoán, không phải để fail. Khi cần ghi lại điều gì đó bất thường nhưng không muốn fail test, console.log xuất hiện trong Postman Console và trong log Newman — rất hữu ích để điều tra sau.

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

Lỗi: bọc pm.test() bên trong try/catch. Nhiều người viết try { pm.test(...) } catch(e){}. Đây là sai lầm ngược đời — nó nuốt mất assertion fail, biến test lẽ ra đỏ thành xanh giả. Nguyên tắc: try/catch bọc code có thể ném lỗi ngoài ý muốn (parse, truy cập thuộc tính lồng sâu), không bọc chính pm.test(). Assertion fail cần được pm.test() báo cáo bình thường.

Lỗi: truy cập thuộc tính lồng sâu không kiểm tra. body.data.user.address.city sẽ ném TypeError nếu bất kỳ mắt xích nào là undefined. Dùng optional chaining của JavaScript hiện đại: body?.data?.user?.address?.city. Postman sandbox hỗ trợ cú pháp này.

Lỗi: dùng pm.expect.fail() thay cho throw khi thực sự cần dừng. Trong pre-request script (Bài 7), đôi khi bạn muốn dừng cả request nếu điều kiện tiên quyết không thỏa. Ở đó throw new Error() là phù hợp. Nhưng trong Tests script, gần như luôn ưu tiên pm.expect.fail() để giữ báo cáo sạch.

Mẹo — phân loại lỗi bằng tag trong thông điệp. Đặt tiền tố như [INFRA], [BUSINESS], [BUG] vào thông điệp fail. Khi lọc log Newman, bạn phân loại được ngay lỗi nào cần retry, lỗi nào cần báo dev.

Mẹo — test flaky thường là test thiếu error handling. Nếu một test "lúc pass lúc fail" trong CI, 80% khả năng là nó không xử lý một nhánh phản hồi hợp lệ (timing, rỗng, rate-limit). Đừng vội disable test — hãy thêm conditional để nó phản ánh đúng thực tế.

Mẹo — luôn có nhánh else cuối cùng. Một chuỗi if/else if mà thiếu else cuối nghĩa là những status bạn chưa lường trước sẽ lặng lẽ pass. Nhánh else với pm.expect.fail() là lưới an toàn bắt những điều bất ngờ.

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

  • Bọc parse an toàn. Lấy một request bất kỳ trong collection của bạn. Viết lại tab Tests theo quy trình 6 bước ở trên: assert status + Content-Type trước, rồi parse body trong try/catch với cờ isJson, rồi gate các test cấu trúc. Cố tình đổi URL sang một trang HTML (ví dụ trang chủ của một website) và xác nhận collection không sập — bạn thấy test skip đúng cách.
  • Conditional theo status. Viết một test cho endpoint "lấy đơn hàng theo ID" xử lý được cả 3 nhánh: 200 (đơn tồn tại → có field total), 404 (không tồn tại → body có error chứa "not found"), và else (fail với thông điệp giàu ngữ cảnh gồm status code và body).
  • Field optional. Cho một response có field discountCode là tùy chọn, viết test khẳng định: nếu field tồn tại thì phải là chuỗi in hoa dài 6–10 ký tự; nếu không tồn tại thì test vẫn pass. Thử với cả hai loại dữ liệu để chắc chắn không có false positive.
  • Nâng cao — phân loại lỗi. Thêm tiền tố [INFRA] / [BUSINESS] / [BUG] vào các thông điệp fail trong bài tập 2, rồi chạy qua Newman và quan sát log. Bạn có thể grep log để đếm số lỗi từng loại không?

Tóm tắt

Error handling và conditional test là thứ biến một bộ test "chạy được trên máy tôi" thành một bộ test đáng tin trong CI/CD. Ba ý cốt lõi cần nhớ:

  • Đừng để test tự sập. Bọc mọi thao tác có thể ném lỗi (đặc biệt là pm.response.json() và truy cập thuộc tính lồng sâu) trong try/catch hoặc optional chaining. Một exception không được bắt sẽ làm im lặng hàng loạt test phía sau và biến báo cáo thành lời nói dối.
  • Test phải biết rẽ nhánh theo ngữ cảnh. Không phải mọi phản hồi khác 200 là bug, không phải mọi field đều bắt buộc. Dùng if/else để áp đúng assertion cho từng nhánh phản hồi hợp lệ, và luôn có nhánh else với pm.expect.fail() làm lưới an toàn.
  • Báo cáo trung thực là mục tiêu tối thượng. pm.test.skip() cho test không áp dụng, pm.expect.fail() với thông điệp giàu ngữ cảnh cho lỗi thật, console.log cho thông tin chẩn đoán. Người đọc báo cáo lúc 3 giờ sáng phải hiểu chuyện gì xảy ra chỉ trong vài giây.
Ở bài tiếp theo, bạn sẽ học cách nhúng Newman thành thư viện ngay trong test Node.js — nơi những kỹ năng xử lý lỗi này càng trở nên quan trọng khi bộ test Postman của bạn hòa vào một pipeline lớn hơn.

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