Mục lục
Xem mục lục
Testing pyramid là gì
Testing pyramid là mô hình chia test thành ba tầng xếp chồng lên nhau theo hình kim tự tháp: tầng dưới cùng rộng nhất (nhiều test nhất), càng lên cao càng hẹp (ít test hơn). Ba tầng đó là unit, integration, E2E.

Ý tưởng cốt lõi: chi phí và tốc độ chạy của một test tỷ lệ nghịch với phạm vi nó bao phủ. Test phạm vi càng nhỏ thì chạy càng nhanh, càng rẻ, càng dễ debug khi fail, nên nên viết nhiều. Test phạm vi càng rộng thì càng gần với hành vi thật của người dùng, nhưng chạy chậm, tốn tài nguyên, dễ flaky, nên chỉ nên giữ ở số lượng vừa đủ cho những luồng quan trọng nhất.
Unit test
Unit test kiểm tra một đơn vị code nhỏ nhất, độc lập với phần còn lại của hệ thống: một function thuần, một custom hook, hoặc một component đơn giản không phụ thuộc API/context bên ngoài.
// utils/formatCurrency.js
export function formatCurrency(amount) {
return new Intl.NumberFormat("vi-VN", {
style: "currency",
currency: "VND",
}).format(amount);
}
// utils/formatCurrency.test.js
import { formatCurrency } from "./formatCurrency";
test("format số thành chuỗi tiền tệ VND", () => {
expect(formatCurrency(150000)).toBe("150.000 ₫");
});
Đặc điểm của unit test:
- Không cần render DOM, không cần mock network, chạy trong vài mili-giây.
- Khi fail, biết chính xác đơn vị code nào sai vì phạm vi kiểm tra rất hẹp.
- Số lượng nên nhiều nhất trong toàn bộ test suite, vì chi phí viết và chạy đều thấp.
Integration test
Integration test kiểm tra nhiều đơn vị phối hợp với nhau: một component gọi API (đã mock), cập nhật state nội bộ, render lại UI dựa trên state mới, người dùng tương tác và component phản hồi đúng.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import UserProfile from "./UserProfile";
const server = setupServer(
http.get("/api/user/1", () => {
return HttpResponse.json({ id: 1, name: "Nguyen Van A" });
})
);
beforeAll(() => server.listen());
afterAll(() => server.close());
test("hiển thị tên user sau khi fetch xong", async () => {
render(<UserProfile userId={1} />);
expect(screen.getByText("Đang tải...")).toBeInTheDocument();
expect(await screen.findByText("Nguyen Van A")).toBeInTheDocument();
});
Trong dự án React/Next.js thực tế, integration test chiếm phần lớn giá trị: đa số bug xảy ra ở chỗ nhiều phần phối hợp sai với nhau (component không xử lý đúng trạng thái loading, form submit sai payload), chứ không phải ở một hàm thuần riêng lẻ. Đây là tầng nên đầu tư nhiều thời gian nhất sau unit test.
E2E test (end-to-end)
E2E test chạy qua trình duyệt thật (hoặc trình duyệt headless), mô phỏng toàn bộ luồng người dùng thật từ đầu đến cuối: mở trang, điền form, click, điều hướng, kiểm tra kết quả cuối cùng.
import { test, expect } from "@playwright/test";
test("người dùng thêm sản phẩm vào giỏ và thanh toán thành công", async ({ page }) => {
await page.goto("/products/1");
await page.getByRole("button", { name: "Thêm vào giỏ" }).click();
await page.goto("/cart");
await page.getByRole("button", { name: "Thanh toán" }).click();
await page.getByLabel("Số thẻ").fill("4242424242424242");
await page.getByRole("button", { name: "Xác nhận" }).click();
await expect(page.getByText("Đặt hàng thành công")).toBeVisible();
}
);
E2E test có giá trị vì nó không mock bất cứ thứ gì (frontend, backend, database thật hoặc gần thật), nên phát hiện được những lỗi mà unit/integration test bỏ sót: sai cấu hình route, backend trả response khác schema mong đợi, lỗi tích hợp giữa các service. Đổi lại, mỗi test E2E chạy chậm hơn unit test hàng trăm đến hàng nghìn lần, và dễ fail vì lý do không liên quan đến bug thật (mạng chậm, phần tử chưa kịp render, dữ liệu test bị trùng).
Bảng so sánh nhanh
| Tiêu chí | Unit | Integration | E2E |
|---|---|---|---|
| Phạm vi | Một function/hook/component đơn lẻ | Nhiều đơn vị phối hợp (component + state + API mock) | Toàn bộ luồng qua trình duyệt thật |
| Tốc độ | Rất nhanh (mili-giây) | Nhanh đến trung bình | Chậm (giây đến chục giây mỗi test) |
| Số lượng nên có | Nhiều nhất | Nhiều, chiếm phần lớn giá trị thực tế | Ít nhất, chỉ luồng quan trọng |
| Khi fail | Biết ngay chỗ sai | Biết được vùng sai, cần khoanh vùng thêm | Cần debug nhiều bước mới ra nguyên nhân gốc |
| Độ giống hành vi thật | Thấp | Trung bình | Cao nhất |
Sai lầm phổ biến: viết quá nhiều E2E test
Một sai lầm thường gặp, đặc biệt ở team mới làm quen với testing, là dùng E2E test để phủ mọi trường hợp, kể cả những case lẽ ra chỉ cần unit hoặc integration test đã đủ (validate một field form, kiểm tra một điều kiện hiển thị/ẩn UI). Hậu quả:
- CI chạy chậm: hàng trăm E2E test cho mọi nhánh logic khiến pipeline mất hàng chục phút, làm chậm vòng lặp phát hiện lỗi.
- Flaky test tăng theo cấp số nhân: E2E test phụ thuộc vào nhiều lớp (network, timing, trạng thái database), càng nhiều test càng dễ có test fail ngẫu nhiên không liên quan tới bug thật, khiến team dần mất niềm tin vào kết quả CI (bấm “chạy lại” thay vì điều tra).
- Khó bảo trì: khi UI thay đổi (đổi text, đổi cấu trúc form), nhiều E2E test cùng fail một lúc, chi phí sửa cao hơn hẳn so với sửa vài unit test tương ứng.
Cách tránh: áp dụng nguyên tắc “shift left”, tức là đẩy việc kiểm tra xuống tầng thấp nhất có thể xử lý được. Trước khi viết một E2E test, tự hỏi: case này có thể kiểm tra bằng unit hay integration test không? Chỉ giữ E2E cho những luồng thực sự cần xác nhận toàn bộ hệ thống hoạt động đúng cùng nhau, ví dụ luồng đăng nhập, luồng thanh toán, luồng tạo đơn hàng, không phải mọi nhánh rẽ trong logic nghiệp vụ.
Tổng kết
- Testing pyramid gồm ba tầng: unit (nhiều nhất, nhanh nhất), integration (chiếm phần lớn giá trị thực tế), E2E (ít nhất, chậm nhất).
- Unit test phù hợp cho logic thuần, độc lập; integration test phù hợp cho component phối hợp state/API; E2E test phù hợp cho luồng nghiệp vụ quan trọng cần xác nhận toàn hệ thống.
- Nguyên tắc chọn tầng: ưu tiên tầng thấp nhất có thể kiểm tra được case đó, chỉ đẩy lên E2E khi thực sự cần độ tin cậy end-to-end.
- Viết quá nhiều E2E test làm chậm CI, tăng flaky test, và khó bảo trì hơn nhiều so với unit/integration test tương đương.