Skip to content
DevRecall
← Roadmap
27

Testing Strategy

drafted

Test 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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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ệmMột câuVí dụ ngắn
Unit testTest một đơn vị logic cô lập, nhanh (ms)calculateDiscount(cart, coupon)
Integration testTest nhiều thành phần thật phối hợp (HTTP + service + DB)Supertest POST /orders → Postgres thật trong container
E2E testTest cả hệ thống qua UI/API như người dùngPlaywright: đăng nhập → thêm giỏ → checkout
Contract testXác minh consumer và provider đồng ý về API/messagePact: consumer sinh contract, provider verify trong CI
Test doubleThứ thay thế dependency thật trong testdummy, stub, spy, mock, fake (Meszaros)
FakeImplementation thật nhưng đơn giảnInMemoryOrderRepository
Flaky testKết quả khác nhau với cùng codePass local, fail CI 1/10 lần do race timer
TestcontainersKhởi động dependency thật (Postgres, Kafka, Redis) bằng Docker trong testnew PostgreSqlContainer("postgres:16").start()
Mutation testingSử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 tra100% 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 testTốc độĐộ tự tinChi phí bảo trìHay vỡ vì
Unit (mock nhiều)Rất nhanhThấp–vừaCao nếu mock implementationRefactor
Unit (logic thuần)Rất nhanhVừaThấpĐổi requirement
Integration (DB thật)Vừa (giây)CaoVừaSchema/fixture
ContractNhanhCao cho ranh giới APIVừa (cần broker, quy trình)Đổi API
E2E UIChậmCao nhất cho journeyCaoSelector, timing, dữ liệu, môi trường
Công cụChọn khiLưu ý
JestCodebase CJS có sẵn, ecosystem lớnESM/TS cần cấu hình transform
VitestDự án Vite/ESM/TS mới, muốn nhanhAPI gần Jest (vi thay jest), migrate dễ
PlaywrightE2E nhiều browser, auto-wait, traceCần quản lý dữ liệu test/auth state
CypressTeam đã quen, DX debug tốtMô 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/sleep cố định trong test async → flaky; dùng findBy*, 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 fetch bằng tay.
  • API test: supertest(app) không cần listen(); 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.

Interview Questions

Showing 46 / 46

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" });
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);
});