Kiểm thử phần mềm
Kiểm thử hợp đồng API đăng ký học phần
Kiểm tra mã HTTP, mã lỗi nghiệp vụ và trạng thái dữ liệu bằng node:test trên một API mẫu.
Thích và đánh giá Bình luận và hỏi đáp1. Viết hợp đồng phản hồi trước khi viết test
Xét một API mô phỏng nhận POST /registrations với mã sinh viên và mã lớp. Yêu cầu thành công tạo một đăng ký và giảm một chỗ trống. Khi thất bại, API trả mã lỗi có thể dùng để quyết định giao diện hiển thị thông báo nào. Mục tiêu của bài là kiểm tra cả mã HTTP, cấu trúc JSON và ảnh hưởng của yêu cầu lên dữ liệu.
| Tình huống | HTTP | Nội dung cần kiểm tra |
|---|---|---|
| Đăng ký thành công | 201 | Đúng mã sinh viên, lớp và số chỗ còn lại |
| Thiếu mã sinh viên hoặc mã lớp | 400 | INVALID_INPUT |
| Lớp không tồn tại | 404 | COURSE_NOT_FOUND |
| Lớp đầy, đăng ký trùng hoặc thiếu tiên quyết | 409 | Mã lỗi riêng cho mỗi nguyên nhân |
Các mã HTTP và thứ tự ưu tiên kiểm tra trong bảng là hợp đồng của API minh họa. Khi nhiều điều kiện cùng sai, cần thống nhất API trả lỗi đầu tiên hay tổng hợp lỗi. Ba tình huống cùng trả 409 vẫn cần ba mã lỗi khác nhau. Nếu phía máy khách chỉ kiểm tra 409, nó có thể nhắc sinh viên học lại môn tiên quyết dù lý do thật là lớp đầy. Bảng phản hồi giúp người viết test kiểm tra được sự khác biệt đó.
2. Chạy bằng node:test trên API trong bộ nhớ
Bài mẫu dùng Node.js 22 hoặc mới hơn. Lệnh node --test dang-ky.test.mjs gọi trực tiếp hàm xử lý với đối tượng Request và đọc Response. Tên miền example.test chỉ là một phần của URL trong dữ liệu kiểm thử. Bài mẫu không gửi yêu cầu qua mạng.
Môi trường thực hành. Node.js 22 trở lên. Chạy node --test cùng tên tệp; các ca mẫu xử lý dữ liệu trong bộ nhớ.
import assert from "node:assert/strict";
import test from "node:test";
function createRegistrationApi() {
const seats = new Map([["CTDL01", 1], ["KTPM01", 0], ["PTTK01", 2]]);
const completedPrerequisite = new Set(["SV01", "SV02"]);
const enrolled = new Set();
return async function handle(request) {
const path = new URL(request.url).pathname;
if (path !== "/registrations") return Response.json({ code: "NOT_FOUND" }, { status: 404 });
if (request.method !== "POST") {
return Response.json({ code: "METHOD_NOT_ALLOWED" }, { status: 405, headers: { Allow: "POST" } });
}
let body;
try {
body = await request.json();
} catch {
return Response.json({ code: "INVALID_JSON" }, { status: 400 });
}
const { studentId, courseId } = body ?? {};
if (typeof studentId !== "string" || !studentId.trim() ||
typeof courseId !== "string" || !courseId.trim()) {
return Response.json({ code: "INVALID_INPUT" }, { status: 400 });
}
if (!seats.has(courseId)) {
return Response.json({ code: "COURSE_NOT_FOUND" }, { status: 404 });
}
const key = JSON.stringify([studentId, courseId]);
if (enrolled.has(key)) {
return Response.json({ code: "ALREADY_ENROLLED" }, { status: 409 });
}
if (!completedPrerequisite.has(studentId)) {
return Response.json({ code: "MISSING_PREREQUISITE" }, { status: 409 });
}
const remaining = seats.get(courseId);
if (remaining === 0) {
return Response.json({ code: "COURSE_FULL" }, { status: 409 });
}
enrolled.add(key);
seats.set(courseId, remaining - 1);
return Response.json({ studentId, courseId, remainingSeats: remaining - 1 }, { status: 201 });
};
}
function post(body) {
return new Request("https://example.test/registrations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
}
test("201 trả dữ liệu đăng ký và số chỗ còn lại", async () => {
const api = createRegistrationApi();
const response = await api(post({ studentId: "SV01", courseId: "CTDL01" }));
assert.equal(response.status, 201);
assert.match(response.headers.get("Content-Type"), /^application\/json/);
assert.deepEqual(await response.json(), {
studentId: "SV01", courseId: "CTDL01", remainingSeats: 0
});
});
test("400 cho dữ liệu thiếu và 404 cho mã lớp sai", async () => {
const api = createRegistrationApi();
for (const [body, status, code] of [
[{ courseId: "CTDL01" }, 400, "INVALID_INPUT"],
[{ studentId: "SV01", courseId: "SAI" }, 404, "COURSE_NOT_FOUND"]
]) {
const response = await api(post(body));
assert.equal(response.status, status);
assert.deepEqual(await response.json(), { code });
}
});
test("409 trả đúng lý do và không làm mất chỗ", async () => {
const api = createRegistrationApi();
const denied = await api(post({ studentId: "SV03", courseId: "CTDL01" }));
assert.equal(denied.status, 409);
assert.deepEqual(await denied.json(), { code: "MISSING_PREREQUISITE" });
const accepted = await api(post({ studentId: "SV01", courseId: "CTDL01" }));
assert.equal(accepted.status, 201);
assert.equal((await accepted.json()).remainingSeats, 0);
});
test("phân biệt đăng ký trùng với lớp đầy", async () => {
const api = createRegistrationApi();
assert.equal((await api(post({ studentId: "SV01", courseId: "CTDL01" }))).status, 201);
const duplicate = await api(post({ studentId: "SV01", courseId: "CTDL01" }));
assert.equal(duplicate.status, 409);
assert.deepEqual(await duplicate.json(), { code: "ALREADY_ENROLLED" });
const full = await api(post({ studentId: "SV02", courseId: "CTDL01" }));
assert.equal(full.status, 409);
assert.deepEqual(await full.json(), { code: "COURSE_FULL" });
});Mỗi ca tạo một bản API mới để giữ dữ liệu độc lập. Tuy nhiên trong một ca, ta có thể chủ động gửi nhiều yêu cầu liên tiếp. Ví dụ, gửi yêu cầu thiếu tiên quyết rồi dùng người đủ điều kiện đăng ký chỗ cuối. Nếu yêu cầu thứ hai thất bại vì hết chỗ, thao tác bị từ chối trước đó đã làm thay đổi dữ liệu.
3. Kiểm tra phản hồi theo từng lớp
- Kiểm tra
statusđể đối chiếu giao kèo HTTP. - Kiểm tra kiểu nội dung hoặc đọc JSON, rồi xác nhận từng trường có ý nghĩa nghiệp vụ.
- Quan sát trạng thái qua một thao tác tiếp theo. Ca kiểm thử dùng số chỗ còn lại và kết quả đăng ký tiếp theo để phát hiện lỗi ghi dữ liệu.
Bốn ca phải đạt. Hãy đổi tạm giá trị trong nhánh COURSE_FULL sang ALREADY_ENROLLED. Ca cuối phải thất bại dù HTTP vẫn là 409. Khôi phục mã và thử thêm yêu cầu JSON sai cú pháp để phủ nhánh INVALID_JSON.
4. Đưa cùng hợp đồng sang kiểm thử tích hợp
Khi có máy chủ thử nghiệm, gửi yêu cầu thật để kiểm tra thêm định tuyến, xác thực, tuần tự hóa và kết nối cơ sở dữ liệu. Có thể chuyển các khẳng định về mã HTTP và mã lỗi sang Postman. Với API thực, tạo dữ liệu thử riêng cho mỗi ca. Nếu hai yêu cầu đồng thời tranh chỗ cuối, tiêu chí mong đợi là chỉ một yêu cầu thành công và dữ liệu không bị âm.
Đọc thêm tại Node.js · Test runner và Postman · Ví dụ kiểm thử phản hồi.