Skip to content
Home
Go back

P1: Testing pyramid: Unit, Integration, E2E khác nhau ở đâu

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.

Mô hình kim tự tháp kiểm thử: E2E Tests ở đỉnh, Integration Tests ở giữa, Unit Tests ở đáy
Mô hình kim tự tháp kiểm thử

Ý 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:

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íUnitIntegrationE2E
Phạm viMộ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ìnhChậm (giây đến chục giây mỗi test)
Số lượng nên cóNhiều nhấtNhiều, chiếm phần lớn giá trị thực tếÍt nhất, chỉ luồng quan trọng
Khi failBiết ngay chỗ saiBiết được vùng sai, cần khoanh vùng thêmCần debug nhiều bước mới ra nguyên nhân gốc
Độ giống hành vi thậtThấpTrung bìnhCao 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ả:

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


Share this post:

Previous Post
Redux: Store, Action, Reducer và Redux Toolkit