Mở đầu — vì sao bài này quan trọng
Đến đây bạn đã đi qua hai project thực tế với API kiểu REST: một cho e-commerce Việt Nam, một cho cổng thanh toán. Bây giờ chúng ta bước vào một thế giới hơi khác — GraphQL. Nếu bạn từng cài Postman, tạo request và thấy chỉ có một cái mục lục các endpoint (/products, /orders, /users...), thì GraphQL sẽ làm bạn ngạc nhiên: gần như mọi thứ đều đi qua một endpoint duy nhất POST /graphql. Không còn 40 route riêng lẻ, chỉ còn một cửa và bạn quyết định lấy gì bằng chính nội dung request.
Vì sao đây là kỹ năng đáng tiền? Vì phần lớn các app mobile hiện đại của Đông Nam Á — Shopee, Grab, Tiki, Lazada — đều dùng GraphQL cho backend-for-frontend (BFF). Lý do rất thực tế: màn hình mobile cần dữ liệu gọn, tránh over-fetching (tải thừa) trên mạng 3G/4G. Một QA hiểu cách test GraphQL sẽ đọc được traffic của chính app mình đang dùng hằng ngày, và quan trọng hơn, viết được bộ test suite mà đội REST-only không làm nổi.
Bài này là một project hoàn chỉnh. Chúng ta sẽ dựng một test suite cho một GraphQL API kiểu Shopee-mobile: xác thực bằng Bearer JWT, khai thác introspection để hiểu schema, rồi viết assertions cho query, mutation và những cạm bẫy rất riêng của GraphQL mà nếu áp máy móc tư duy REST vào, bạn sẽ test sai mà không hề hay biết.
Khái niệm cốt lõi
Một endpoint, ba loại operation
GraphQL chỉ có một URL nhưng ba kiểu thao tác nằm trong body của request:
- Query — đọc dữ liệu (tương đương GET).
- Mutation — thay đổi dữ liệu (tương đương POST/PUT/DELETE).
- Subscription — nhận cập nhật realtime (thường qua WebSocket, ta chạm nhẹ ở bài WebSocket riêng, project này tập trung query/mutation).
{
"query": "query GetProduct($id: ID!) { product(id: $id) { name price } }",
"variables": { "id": "SP12345" }
}
Chú ý: trường query chứa cả query lẫn mutation (đừng để tên gây nhầm). Biến đi trong variables — bạn không nối chuỗi biến vào query, vì nối chuỗi là mở cửa cho injection và làm test khó tái sử dụng.
Cạm bẫy lớn nhất: HTTP 200 không có nghĩa là thành công
Đây là điều khiến QA quen REST hay sai. Trong REST, bạn assert pm.response.to.have.status(200) là gần như xong. Trong GraphQL, server trả 200 cho cả request thành công lẫn request lỗi logic. Lỗi nằm trong body, ở mảng errors:
{
"data": { "product": null },
"errors": [
{ "message": "Product not found", "extensions": { "code": "NOT_FOUND" } }
]
}
Nếu test của bạn chỉ kiểm tra status 200, nó sẽ báo PASS trong khi API thực chất đang lỗi. Nguyên tắc vàng của project này: luôn assert cả data lẫn sự vắng mặt của errors (hoặc sự hiện diện của errors đúng như kỳ vọng khi test negative case).
Introspection — bản đồ của schema
GraphQL cho phép hỏi chính server về cấu trúc của nó qua introspection query (__schema, __type). Với QA, đây là công cụ vàng để: biết field nào tồn tại, kiểu dữ liệu gì, argument nào bắt buộc — trước khi viết một dòng test. Ở nhiều công ty, introspection bị tắt trên production vì lý do bảo mật, nhưng thường bật ở staging. Bài học thực tế: hãy chạy introspection trên staging để build test, rồi có một test riêng assert rằng introspection đã bị tắt trên production.
Tình huống thực tế
Ví dụ 1 — Đội QA Tiki và cái bẫy "200 OK màu xanh giả tạo"
Một bạn QA mới ở đội catalog của Tiki được giao regression suite cho GraphQL API mobile. Bạn ấy copy tư duy từ suite REST cũ: mỗi request assert đúng một dòng status 200. Suite chạy 120 test, xanh mướt, ai cũng vui. Ba tuần sau, khách hàng báo màn hình chi tiết sản phẩm hiển thị "Giá: null" cho hàng nghìn SKU.
Điều tra ra: một thay đổi backend khiến field price trả về lỗi resolver — body có errors với message "Currency service timeout", data.product.price bằng null, nhưng HTTP vẫn 200. Suite của bạn ấy không hề bắt được vì nó chưa bao giờ nhìn vào errors.
Bài học rút ra: trong GraphQL, status code gần như vô dụng cho việc kiểm tra tính đúng đắn. Test suite phải mặc định có một assertion "không có errors" cho mọi happy-path query, và assert giá trị field thực sự khác null khi field đó là bắt buộc.
Ví dụ 2 — Startup fintech ở TP.HCM và over-fetching tốn tiền 4G
Một startup ví điện tử (giả định tên "PayGo") xây app mobile trên GraphQL. Đội frontend than app tốn data của người dùng ở tỉnh dùng 4G. QA lead nghi ngờ backend trả thừa. Bạn ấy viết một test đặc thù GraphQL mà REST không có: gửi một query chỉ xin đúng 3 field (id, balance, currency) rồi assert rằng response không chứa các field nặng như transactionHistory.
Kết quả lộ ra một bug: backend resolver luôn eager-load toàn bộ lịch sử giao dịch bất kể client xin gì, khiến payload phình từ 2KB lên 340KB mỗi lần mở app. Nhờ test này, đội fix resolver để tôn trọng field selection, giảm 99% payload.
Bài học: sức mạnh của GraphQL là client chọn field, và test suite nên khai thác chính đặc tính đó — assert cả field có mặt (bạn xin thì phải có) lẫn field vắng mặt (bạn không xin thì không được trả).
Ví dụ 3 — Sàn TMĐT và mutation "đặt hàng hai lần"
Một sàn kiểu Shopee có mutation createOrder. Đội QA test happy path: gọi mutation, assert order được tạo, xong. Nhưng khi lên production, có khách bấm nút đặt hàng hai lần do mạng lag, và hệ thống tạo hai đơn trùng.
QA bổ sung vào suite một kịch bản chained: (1) mutation createOrder lấy orderId, (2) query lại đơn đó xác nhận trạng thái, (3) gọi lại createOrder với cùng idempotencyKey và assert rằng server trả về cùng orderId cũ chứ không tạo đơn mới. Đây là chỗ GraphQL testing giao với tư duy idempotency ta học ở bài khác — nhưng trong ngữ cảnh mutation.
Bài học: mutation phải được test không chỉ ở "có tạo được không" mà còn ở "gọi lại thì có nhân bản dữ liệu không". Đây là loại bug đắt giá nhất trong thanh toán.
Hướng dẫn từng bước
Ta dựng suite theo trình tự sau. Giả định endpoint là POST https://staging-api.shopee-clone.vn/graphql.
Bước 1 — Dựng collection và environment. Tạo collection "GraphQL Suite - Mobile API". Tạo environment staging với biến baseUrl = https://staging-api.shopee-clone.vn, token (để trống, sẽ điền sau khi login). Đặt Authorization ở cấp collection: type Bearer Token, value {{token}} — mọi request con kế thừa, khỏi lặp lại.
Bước 2 — Login lấy JWT. Tạo request đầu tiên Auth - Login. Trong Postman, chọn body dạng GraphQL (Postman có tab GraphQL riêng), hoặc body raw JSON:
{
"query": "mutation Login($email: String!, $pw: String!) { login(email: $email, password: $pw) { token user { id } } }",
"variables": { "email": "qa@paygo.vn", "pw": "Test@1234" }
}
Tests script để bắt token và không có lỗi:
const res = pm.response.json();
pm.test("Login không có GraphQL errors", () => {
pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;
});
pm.test("Nhận được JWT", () => {
pm.expect(res.data.login.token).to.be.a("string").and.not.empty;
});
pm.environment.set("token", res.data.login.token);
Bước 3 — Chạy introspection để hiểu schema. Tạo request Schema - Introspection:
{ "query": "{ __schema { queryType { fields { name } } mutationType { fields { name } } } }" }
Assert rằng các operation bạn cần (product, createOrder...) có mặt. Đây vừa là bước khám phá, vừa là contract test nhẹ: nếu backend đổi tên field, test này đỏ ngay.
Bước 4 — Test query (happy path + field selection). Request Query - Product:
{
"query": "query($id: ID!){ product(id:$id){ id name price stock } }",
"variables": { "id": "SP12345" }
}
Tests:
const res = pm.response.json();
pm.test("Status 200", () => pm.response.to.have.status(200));
pm.test("Không có errors", () => pm.expect(res.errors).to.be.undefined);
const p = res.data.product;
pm.test("Trả đúng sản phẩm được xin", () => {
pm.expect(p).to.have.all.keys("id", "name", "price", "stock");
pm.expect(p.id).to.eql("SP12345");
pm.expect(p.price).to.be.a("number").and.above(0);
});
Chú ý have.all.keys — nó vừa kiểm tra bạn nhận đúng field đã xin, vừa bắt được over-fetching như ví dụ PayGo.
Bước 5 — Test negative case (errors đúng kỳ vọng). Request Query - Product Not Found với id không tồn tại. Ở đây bạn muốn có errors:
const res = pm.response.json();
pm.test("Vẫn HTTP 200 (đặc tính GraphQL)", () => pm.response.to.have.status(200));
pm.test("Có error NOT_FOUND", () => {
pm.expect(res.errors).to.be.an("array").that.is.not.empty;
pm.expect(res.errors[0].extensions.code).to.eql("NOT_FOUND");
});
pm.test("data.product là null", () => pm.expect(res.data.product).to.be.null);
Bước 6 — Test mutation với chained workflow. Request Mutation - Create Order gọi createOrder, lưu orderId vào biến, rồi request tiếp theo query lại đơn để xác nhận. Với idempotency, thêm một request gọi lại cùng idempotencyKey và assert cùng orderId.
Bước 7 — Test authorization. Tạo một request cố ý xóa/đổi token thành rỗng hoặc giả, gọi một field cần đăng nhập, và assert errors[0].extensions.code là UNAUTHENTICATED. Đây là test bảo mật tối thiểu mọi suite GraphQL cần có.
Bước 8 — Test introspection bị tắt trên production. Trỏ sang baseUrl production, chạy introspection query, assert rằng nó bị từ chối (có errors kiểu "GraphQL introspection is not allowed"). Nếu production vẫn cho introspection, đó là một phát hiện bảo mật đáng báo cáo.
Lỗi thường gặp & mẹo
Chỉ assert status 200. Đã nói ở trên nhưng nhắc lại vì nó là lỗi số một. Hãy tạo một snippet dùng lại: bỏ một hàm assertNoErrors(res) vào collection-level pre-request hoặc dùng pm.collectionVariables chứa helper. Mọi happy path đều gọi nó.
Nối biến vào chuỗi query thay vì dùng variables. Vừa dễ lỗi cú pháp GraphQL (thiếu dấu ngoặc kép quanh string), vừa mở đường cho injection. Luôn tách variables ra JSON riêng.
Quên rằng field có thể null hợp lệ. Trong GraphQL, một field không có dấu ! trong schema là nullable — trả null là đúng chuẩn. Đừng assert not.null cho field nullable; hãy dùng introspection để biết field nào bắt buộc (non-null) mà đặt assertion cho đúng.
Không xử lý partial data. GraphQL có thể trả cả data (một phần) lẫn errors cùng lúc — ví dụ product có nhưng field reviews lỗi resolver. Test cứng nhắc "hoặc data hoặc errors" sẽ hiểu sai. Với các query phức tạp, hãy assert cụ thể từng field bạn quan tâm.
Mẹo dùng tab GraphQL của Postman. Postman có body type "GraphQL" riêng, tự autocomplete field dựa trên introspection và tách query/variables gọn gàng. Với data-driven testing (chạy nhiều id từ CSV như bài Data-driven đã học), bạn vẫn tham số hóa qua variables và Newman iterate được như thường.
Mẹo rate limiting. GraphQL cho phép query lồng sâu, dễ vô tình tạo query "bom" (nested cực sâu). Nhiều backend giới hạn độ sâu/complexity. Thêm một test cố ý gửi query lồng 15 tầng và assert server trả lỗi QUERY_TOO_COMPLEX — vừa kiểm tra phòng thủ của backend.
Bài tập thực hành
- Dựng collection GraphQL Suite hoàn chỉnh cho một public GraphQL API để luyện tay:
https://countries.trevorblades.com/(không cần auth). Viết query lấycountry(code:"VN"){ name capital currency }và assertnamelà "Vietnam",capitallà "Hanoi", không cóerrors.
- Với API đó, viết một test field-selection: xin đúng
codevàname, dùnghave.all.keysđể assert response không chứa field thừa nhưlanguages.
- Viết negative test: query
country(code:"ZZZ")(mã không tồn tại) và assertdata.countrylànull.
- Giả lập chained workflow: nếu bạn có sandbox mutation (hoặc mock server tự dựng từ bài Mock Servers), tạo
createOrder→ lưuorderId→ query lại đơn → assert trạng tháiPENDING. Thêm một vòng gọi lại với cùngidempotencyKeyvà assert cùngorderId.
- Viết một introspection test list toàn bộ mutation của API và ghi ra Postman console. Sau đó thử trên một endpoint production bất kỳ và quan sát introspection có bị tắt không.
- Chạy toàn bộ suite qua Newman với reporter HTML (bài Newman Reporters) và đối chiếu: có test nào PASS chỉ nhờ status 200 mà lẽ ra phải FAIL không?
Tóm tắt
GraphQL đảo ngược nhiều giả định của REST testing. Chỉ một endpoint POST /graphql, ba loại operation nằm trong body, và cạm bẫy chí mạng là HTTP 200 không đồng nghĩa với thành công — lỗi nằm trong mảng errors. Một test suite GraphQL nghiêm túc phải: assert cả data lẫn errors cho mọi happy path; khai thác field selection để bắt over-fetching; test negative case với errors đúng kỳ vọng và extensions.code cụ thể; chained mutation kèm kiểm tra idempotency; và dùng introspection vừa để khám phá schema vừa để xác minh nó bị tắt trên production.
Ba tình huống Tiki, PayGo và sàn TMĐT cho thấy cùng một thông điệp: đừng bê nguyên tư duy REST sang GraphQL. Khi bạn viết được bộ suite này, bạn không chỉ test được API của Shopee-clone mà còn nắm một kỹ năng mà đa số QA thị trường còn thiếu — và đó chính là bước đệm để tiến tới vai trò SDET mà lộ trình cuối khóa sẽ nói tới.