const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), "a@b.com");
await user.click(screen.getByRole("button", { name: /sign in/i }));
expect(onSubmit).toHaveBeenCalledWith({ email: "a@b.com" });
Testing Strategy
draftedTest pyramid vs trophy, unit/integration/e2e/contract (Pact), Jest/Vitest, Supertest, React Testing Library, Playwright, test doubles, testcontainers, flaky tests, TDD, coverage, test auth & tenant isolation, test Kafka consumer, mutation testing.
0/46 rated · mastery 0%
Bài học
6 bài · ~74 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~14 phút· 4 câu
Mục tiêu của test, pyramid, trophy và honeycomb
Test để làm gì, bốn loại test (unit, integration, e2e, contract) định nghĩa theo ranh giới, và vì sao pyramid, trophy và honeycomb bất đồng về tỉ lệ nhưng đồng ý về e2e.
- 02~12 phút· 4 câuverify
Test doubles: dummy, stub, spy, mock, fake
Phân loại test double của Meszaros, state vs behaviour verification, vi.fn/jest.fn thực chất là gì, các gotcha của vi.mock/jest.mock (hoisting, mock cả module, state rò), và vì sao mock database cho test xanh mà production rò dữ liệu tenant.
- 03~15 phút· 4 câuverify
Jest vs Vitest, fake timers và snapshot
Runner hoạt động ra sao, khác biệt ESM/TS, defaults (clearMocks), thời gian (inject clock, fake timers) và khi nào snapshot có ích.
- 04~12 phút· 4 câuverify
API integration test: Supertest và database thật
Supertest không cần listen, Testcontainers Postgres, mock vs in-memory vs engine thật, và test chính migration.
- 05~11 phút· 5 câuverify
Cô lập test, test data và flaky test
Rollback vs truncate vs DB-per-worker, factory, chạy song song, và quy trình đo-quarantine-fix flaky test.
- 06~11 phút· 4 câuverify
Test React với Testing Library và MSW
Nguyên tắc RTL, thứ tự query, user-event, findBy vs sleep, act warning, MSW, và test Next.js App Router.
Tóm tắt & cheat sheet của track
TL;DR
- Mục tiêu của test không phải coverage mà là tự tin để thay đổi code và deploy — test tốt bắt bug thật, chạy nhanh, ít flaky, và không vỡ khi refactor (test hành vi, không test implementation).
- Pyramid (nhiều unit, ít e2e) vs trophy (static + nhiều integration) — với backend Node gọi DB và frontend React, integration test với dependency thật (testcontainers, RTL render component thật) thường cho tỉ lệ tự tin/chi phí tốt nhất.
- Senior interview xoáy vào: mock cái gì và không mock cái gì, test với DB thật, flaky test, contract test giữa microservices, test auth & tenant isolation, test consumer Kafka (idempotency, retry), giới hạn của coverage.
- Công cụ cần nói được: Jest/Vitest, Supertest, React Testing Library (+ user-event, MSW), Playwright, Testcontainers, Pact, Stryker, k6 (xem track Observability).
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Unit test | Test một đơn vị logic cô lập, nhanh (ms) | calculateDiscount(cart, coupon) |
| Integration test | Test nhiều thành phần thật phối hợp (HTTP + service + DB) | Supertest POST /orders → Postgres thật trong container |
| E2E test | Test cả hệ thống qua UI/API như người dùng | Playwright: đăng nhập → thêm giỏ → checkout |
| Contract test | Xác minh consumer và provider đồng ý về API/message | Pact: consumer sinh contract, provider verify trong CI |
| Test double | Thứ thay thế dependency thật trong test | dummy, stub, spy, mock, fake (Meszaros) |
| Fake | Implementation thật nhưng đơn giản | InMemoryOrderRepository |
| Flaky test | Kết quả khác nhau với cùng code | Pass local, fail CI 1/10 lần do race timer |
| Testcontainers | Khởi động dependency thật (Postgres, Kafka, Redis) bằng Docker trong test | new PostgreSqlContainer("postgres:16").start() |
| Mutation testing | Sửa code có chủ đích (mutant) xem test có bắt được không | Đổi >= thành > mà test vẫn xanh → test yếu |
| Coverage | % code được chạy bởi test — không phải % được kiểm tra | 100% line coverage với test không có assertion |
Cơ chế hoạt động
Chọn loại test cho một thay đổi:
Consumer-driven contract testing (Pact):
Trade-offs
| Loại test | Tốc độ | Độ tự tin | Chi phí bảo trì | Hay vỡ vì |
|---|---|---|---|---|
| Unit (mock nhiều) | Rất nhanh | Thấp–vừa | Cao nếu mock implementation | Refactor |
| Unit (logic thuần) | Rất nhanh | Vừa | Thấp | Đổi requirement |
| Integration (DB thật) | Vừa (giây) | Cao | Vừa | Schema/fixture |
| Contract | Nhanh | Cao cho ranh giới API | Vừa (cần broker, quy trình) | Đổi API |
| E2E UI | Chậm | Cao nhất cho journey | Cao | Selector, timing, dữ liệu, môi trường |
| Công cụ | Chọn khi | Lưu ý |
|---|---|---|
| Jest | Codebase CJS có sẵn, ecosystem lớn | ESM/TS cần cấu hình transform |
| Vitest | Dự án Vite/ESM/TS mới, muốn nhanh | API gần Jest (vi thay jest), migrate dễ |
| Playwright | E2E nhiều browser, auto-wait, trace | Cần quản lý dữ liệu test/auth state |
| Cypress | Team đã quen, DX debug tốt | Mô hình chạy trong browser, hạn chế multi-tab/origin |
Pitfalls
- Mock mọi thứ (kể cả DB/ORM) → test xanh nhưng query SQL sai, migration sai, constraint không được test.
- Test implementation (gọi hàm private, assert số lần gọi nội bộ) → vỡ khi refactor, không bắt bug.
setTimeout/sleepcố định trong test async → flaky; dùngfindBy*,waitFor, web-first assertions.- Test chia sẻ state (DB không reset, biến module) → phụ thuộc thứ tự chạy.
- Coverage là KPI → test không có assertion để đạt số.
- E2E cho mọi case → pipeline 1 giờ, flaky, bị bỏ qua.
- Không test đường lỗi: timeout, retry, 401/403, tenant khác, message trùng.
- Retry flaky test tự động mà không bao giờ fix → che giấu race condition thật trong code.
Cheat sheet
- Query RTL theo ưu tiên:
getByRole>getByLabelText>getByText>getByTestId. Async:findBy*. User action:userEvent.setup(). - Network ở FE/Node test: MSW thay vì mock
fetchbằng tay. - API test:
supertest(app)không cầnlisten(); DB thật qua testcontainers; mỗi test tự tạo dữ liệu, reset bằng transaction rollback hoặc truncate. - Double: stub (trả giá trị), spy (ghi nhận lời gọi), mock (kỳ vọng lời gọi), fake (implementation nhẹ). Chỉ mock ranh giới bạn không sở hữu (payment, email, clock).
- Thời gian: fake timers / inject clock; ngẫu nhiên: seed; ID: inject generator.
- Flaky: đo flaky rate, quarantine có owner + hạn, fix gốc (race, order, time, shared state, network).
- Contract test cho ranh giới giữa service/team; e2e chỉ cho vài journey trọng yếu.
- Auth/tenant: test ma trận role × resource × tenant ở tầng API; test "tenant A không đọc được data tenant B" cho mọi endpoint list/get/update/delete.
- Kafka consumer: tách handler thuần để unit test; integration với Kafka/Redpanda container; test duplicate, out-of-order, poison message → DLQ.
- Mutation score (Stryker) đo chất lượng test tốt hơn line coverage.
References
- Jest — Getting started ↗
- Jest — Timer mocks ↗
- Vitest — Guide ↗
- Vitest — Mocking ↗
- Testing Library — Guiding principles ↗
- Testing Library — Which query should I use? ↗
- Testing Library — user-event ↗
- Playwright — Best practices ↗
- Playwright — Authentication ↗
- Testcontainers for Node.js ↗
- Pact — Introduction to contract testing ↗
- Stryker Mutator — JS/TS ↗
- Mock Service Worker (MSW) ↗
- Martin Fowler — Test doubles (Mocks Aren't Stubs) ↗
- Next.js — Testing ↗
Interview Questions
Showing 46 / 46
import request from "supertest";
import { app } from "../src/app";
it("rejects an order without items", async () => {
const res = await request(app)
.post("/orders")
.set("Authorization", `Bearer ${await tokenFor({ tenantId: "t1", role: "buyer" })}`)
.send({ items: [] });
expect(res.status).toBe(400);
expect(res.body.error.code).toBe("ORDER_EMPTY");
});
jest.mock("../src/db", () => ({
query: jest.fn().mockResolvedValue({ rows: [{ id: 1, total: 100 }] }),
}));
import { query } from "../src/db";
import { getOrdersForTenant } from "../src/orders";
it("returns orders for tenant", async () => {
const orders = await getOrdersForTenant("t1");
expect(query).toHaveBeenCalled();
expect(orders).toHaveLength(1);
});
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
let pg: StartedPostgreSqlContainer;
beforeAll(async () => {
pg = await new PostgreSqlContainer("postgres:16-alpine").start();
process.env.DATABASE_URL = pg.getConnectionUri();
await runMigrations(process.env.DATABASE_URL);
}, 60_000);
afterAll(() => pg.stop());
it("shows search results", async () => {
render(<ProductSearch />);
fireEvent.change(screen.getByRole("searchbox"), { target: { value: "shoe" } });
await new Promise((r) => setTimeout(r, 500)); // wait for debounce + fetch
expect(screen.getByText("Running Shoe")).toBeInTheDocument();
});
it.each([
["buyer", "GET", "/admin/tenants", 403],
["merchant_admin", "GET", "/admin/tenants", 403],
["platform_admin", "GET", "/admin/tenants", 200],
])("%s %s %s -> %i", async (role, method, path, status) => {
const token = await tokenFor({ role, tenantId: "t1" });
const res = await request(app)[method.toLowerCase() as "get"](path).set("Authorization", `Bearer ${token}`);
expect(res.status).toBe(status);
});
import { app } from "../src/app";
import { cache } from "../src/cache"; // module-level Map
describe("GET /products/:id", () => {
it("returns 404 for unknown product", async () => {
const res = await request(app).get("/products/999");
expect(res.status).toBe(404);
});
});
// another file, runs earlier in the same worker:
it("caches product 999 after creating it", async () => {
await request(app).post("/products").send({ id: 999, name: "Tmp" });
await request(app).get("/products/999");
expect(cache.has("product:999")).toBe(true);
});
it("stores the order when OrderPlaced is consumed", async () => {
await producer.send({ topic: "orders", messages: [{ value: JSON.stringify(event) }] });
setTimeout(async () => {
const row = await db.query("SELECT * FROM orders WHERE id = $1", [event.id]);
expect(row.rows).toHaveLength(1);
}, 2000);
});