Hướng dẫn toàn diện về testing và đảm bảo chất lượng phần mềm với Claude Code CLI.
/\
/ \ E2E Tests (ít nhất, chậm nhất, đắt nhất)
/ E2E\ → Kiểm tra toàn bộ luồng người dùng
/------\
/ \ Integration Tests (vừa phải)
/ Integra- \ → Kiểm tra tương tác giữa các module
/ tion \
/--------------\
/ \ Unit Tests (nhiều nhất, nhanh nhất, rẻ nhất)
/ Unit Tests \ → Kiểm tra từng hàm/component riêng lẻ
\__________________/
| Loại test | Tỷ lệ | Thời gian chạy | Độ tin cậy |
|---|---|---|---|
| Unit | 70% | Mili giây | Cao |
| Integration | 20% | Giây | Trung bình |
| E2E | 10% | Phút | Thấp (flaky) |
# Next.js / React
npm install -D jest @testing-library/react @testing-library/jest-dom
npm install -D @types/jest ts-jest jest-environment-jsdom
# Node.js thuần
npm install -D jest @types/jest ts-jest
// jest.config.js
const nextJest = require("next/jest");
const createJestConfig = nextJest({ dir: "./" });
const customJestConfig = {
setupFilesAfterSetup: ["<rootDir>/jest.setup.ts"],
testEnvironment: "jest-environment-jsdom",
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
module.exports = createJestConfig(customJestConfig);
// jest.setup.ts
import "@testing-library/jest-dom";
// lib/utils.ts
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat("vi-VN", {
style: "currency",
currency: "VND",
}).format(amount);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export function calculateDiscount(price: number, discountPercent: number): number {
if (price < 0 || discountPercent < 0 || discountPercent > 100) {
throw new Error("Giá trị không hợp lệ");
}
return Math.round(price * (1 - discountPercent / 100));
}
// __tests__/lib/utils.test.ts
import { formatCurrency, slugify, calculateDiscount } from "@/lib/utils";
describe("formatCurrency", () => {
it("định dạng tiền VNĐ đúng", () => {
expect(formatCurrency(1000000)).toContain("1.000.000");
});
it("xử lý số 0", () => {
expect(formatCurrency(0)).toContain("0");
});
it("xử lý số thập phân", () => {
const result = formatCurrency(99999.5);
expect(result).toBeDefined();
});
});
describe("slugify", () => {
it("chuyển tiếng Việt thành slug", () => {
expect(slugify("Điện thoại iPhone 15")).toBe("dien-thoai-iphone-15");
});
it("xóa ký tự đặc biệt", () => {
expect(slugify("Hello, World!")).toBe("hello-world");
});
it("xử lý chuỗi rỗng", () => {
expect(slugify("")).toBe("");
});
});
describe("calculateDiscount", () => {
it("tính giá sau giảm đúng", () => {
expect(calculateDiscount(1000000, 20)).toBe(800000);
});
it("giảm 0% trả về giá gốc", () => {
expect(calculateDiscount(500000, 0)).toBe(500000);
});
it("giảm 100% trả về 0", () => {
expect(calculateDiscount(500000, 100)).toBe(0);
});
it("ném lỗi khi giá âm", () => {
expect(() => calculateDiscount(-100, 10)).toThrow("Giá trị không hợp lệ");
});
it("ném lỗi khi phần trăm > 100", () => {
expect(() => calculateDiscount(100, 150)).toThrow("Giá trị không hợp lệ");
});
});
// __tests__/components/product-card.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { ProductCard } from "@/components/product-card";
const mockProduct = {
id: "1",
name: "iPhone 15 Pro",
price: 28990000,
image: "/iphone-15.jpg",
slug: "iphone-15-pro",
};
describe("ProductCard", () => {
it("hiển thị tên sản phẩm", () => {
render(<ProductCard product={mockProduct} />);
expect(screen.getByText("iPhone 15 Pro")).toBeInTheDocument();
});
it("hiển thị giá đã format", () => {
render(<ProductCard product={mockProduct} />);
expect(screen.getByText(/28.990.000/)).toBeInTheDocument();
});
it("gọi onAddToCart khi bấm nút", () => {
const handleAddToCart = jest.fn();
render(<ProductCard product={mockProduct} onAddToCart={handleAddToCart} />);
fireEvent.click(screen.getByRole("button", { name: /thêm vào giỏ/i }));
expect(handleAddToCart).toHaveBeenCalledWith(mockProduct.id);
});
it("hiển thị badge 'Hết hàng' khi stock = 0", () => {
render(<ProductCard product= />);
expect(screen.getByText("Hết hàng")).toBeInTheDocument();
});
});
// utils/calculator_test.go
package utils
import "testing"
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
price float64
discount float64
want float64
wantErr bool
}{
{"giảm 20%", 1000000, 20, 800000, false},
{"giảm 0%", 500000, 0, 500000, false},
{"giảm 100%", 500000, 100, 0, false},
{"giá âm", -100, 10, 0, true},
{"phần trăm > 100", 100, 150, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CalculateDiscount(tt.price, tt.discount)
if (err != nil) != tt.wantErr {
t.Errorf("CalculateDiscount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
}
})
}
}
# Chạy tests
go test ./...
# Với coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Chạy test cụ thể
go test -run TestCalculateDiscount ./utils/
# Verbose
go test -v ./...
// __tests__/api/users.test.ts
import { createServer } from "http";
import { apiResolver } from "next/dist/server/api-utils/node";
import request from "supertest";
import { GET, POST } from "@/app/api/users/route";
import { prisma } from "@/lib/prisma";
// Helper tạo test server
function createTestServer(handler: any) {
return createServer(async (req, res) => {
return apiResolver(req, res, undefined, handler, {}, false);
});
}
describe("API /api/users", () => {
beforeEach(async () => {
// Dọn dẹp database test trước mỗi test
await prisma.user.deleteMany();
});
afterAll(async () => {
await prisma.$disconnect();
});
describe("GET /api/users", () => {
it("trả về danh sách rỗng khi chưa có user", async () => {
const res = await request(createTestServer(GET)).get("/api/users");
expect(res.status).toBe(200);
expect(res.body.users).toHaveLength(0);
});
it("trả về danh sách users có phân trang", async () => {
// Tạo test data
await prisma.user.createMany({
data: [
{ email: "user1@test.com", name: "User 1" },
{ email: "user2@test.com", name: "User 2" },
],
});
const res = await request(createTestServer(GET))
.get("/api/users?page=1&limit=10");
expect(res.status).toBe(200);
expect(res.body.users).toHaveLength(2);
});
});
describe("POST /api/users", () => {
it("tạo user mới thành công", async () => {
const res = await request(createTestServer(POST))
.post("/api/users")
.send({ email: "new@test.com", name: "New User" });
expect(res.status).toBe(201);
expect(res.body.email).toBe("new@test.com");
});
it("trả về lỗi khi email trùng", async () => {
await prisma.user.create({
data: { email: "exists@test.com", name: "Existing" },
});
const res = await request(createTestServer(POST))
.post("/api/users")
.send({ email: "exists@test.com", name: "Duplicate" });
expect(res.status).toBe(409);
});
});
});
// test/helpers/db.ts
import { PrismaClient } from "@prisma/client";
import { execSync } from "child_process";
const prisma = new PrismaClient();
// Tạo database test riêng
export async function setupTestDB() {
const testDbUrl = process.env.DATABASE_URL?.replace(
/\/[^/]+$/,
"/test_db"
);
process.env.DATABASE_URL = testDbUrl;
execSync("npx prisma migrate deploy", { env: process.env });
}
// Dọn dẹp sau mỗi test
export async function cleanupTestDB() {
const tables = await prisma.$queryRaw<Array<{ tablename: string }>>`
SELECT tablename FROM pg_tables WHERE schemaname = 'public'
`;
for (const { tablename } of tables) {
if (tablename !== "_prisma_migrations") {
await prisma.$executeRawUnsafe(
`TRUNCATE TABLE "${tablename}" CASCADE`
);
}
}
}
# Cài đặt Playwright
npm init playwright@latest
# Cài đặt browsers
npx playwright install --with-deps
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
["html"],
["list"],
process.env.CI ? ["github"] : ["line"],
],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "mobile",
use: { ...devices["iPhone 14"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
// e2e/auth.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Xác thực người dùng", () => {
test("đăng ký tài khoản mới", async ({ page }) => {
await page.goto("/register");
await page.fill('input[name="name"]', "Nguyễn Văn Test");
await page.fill('input[name="email"]', `test_${Date.now()}@example.com`);
await page.fill('input[name="password"]', "SecurePass123!");
await page.fill('input[name="confirmPassword"]', "SecurePass123!");
await page.click('button[type="submit"]');
// Chờ redirect về dashboard
await expect(page).toHaveURL("/dashboard");
await expect(page.locator("h1")).toContainText("Dashboard");
});
test("đăng nhập thành công", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "admin@example.com");
await page.fill('input[name="password"]', "Admin123!");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
});
test("hiển thị lỗi khi đăng nhập sai", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "wrong@example.com");
await page.fill('input[name="password"]', "wrongpassword");
await page.click('button[type="submit"]');
await expect(page.locator(".error-message")).toContainText(
"Email hoặc mật khẩu không đúng"
);
});
test("redirect về login khi chưa đăng nhập", async ({ page }) => {
await page.goto("/dashboard");
await expect(page).toHaveURL(/\/login/);
});
});
// e2e/products.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Quản lý sản phẩm", () => {
// Đăng nhập trước mỗi test
test.beforeEach(async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "admin@example.com");
await page.fill('input[name="password"]', "Admin123!");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
});
test("tạo sản phẩm mới", async ({ page }) => {
await page.goto("/dashboard/products/new");
await page.fill('input[name="name"]', "Sản phẩm Test");
await page.fill('input[name="price"]', "500000");
await page.fill('textarea[name="description"]', "Mô tả sản phẩm test");
await page.selectOption('select[name="category"]', "electronics");
await page.click('button[type="submit"]');
// Kiểm tra thông báo thành công
await expect(page.locator(".toast-success")).toContainText("Tạo sản phẩm thành công");
});
test("tìm kiếm sản phẩm", async ({ page }) => {
await page.goto("/dashboard/products");
await page.fill('input[name="search"]', "iPhone");
await page.keyboard.press("Enter");
// Chờ kết quả
await expect(page.locator(".product-list")).not.toBeEmpty();
const items = page.locator(".product-item");
await expect(items.first()).toContainText("iPhone");
});
});
# Chạy tất cả tests
npx playwright test
# Chạy test cụ thể
npx playwright test e2e/auth.spec.ts
# Chạy với UI mode
npx playwright test --ui
# Xem report
npx playwright show-report
# Debug mode
npx playwright test --debug
# Chạy trên browser cụ thể
npx playwright test --project=chromium
### Đăng nhập
POST http://localhost:3000/api/auth/login
Content-Type: application/json
{
"email": "admin@example.com",
"password": "Admin123!"
}
### Lấy danh sách sản phẩm
GET http://localhost:3000/api/products?page=1&limit=10
Authorization: Bearer
### Tạo sản phẩm
POST http://localhost:3000/api/products
Authorization: Bearer
Content-Type: application/json
{
"name": "Sản phẩm mới",
"price": 500000,
"categoryId": "uuid-here"
}
Tạo bộ test toàn diện cho API /api/products:
- Test tất cả HTTP methods (GET, POST, PUT, DELETE)
- Test validation (thiếu field, sai format, giá trị không hợp lệ)
- Test authentication (có token, không token, token hết hạn)
- Test authorization (user thường vs admin)
- Test pagination, filtering, sorting
- Test edge cases (ID không tồn tại, data trùng lặp)
- Test rate limiting
Dùng Jest + Supertest cho Node.js.
# Cài đặt k6
sudo apt install k6
# hoặc
brew install k6
// load-tests/api-test.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 20 }, // Tăng dần lên 20 users
{ duration: "1m", target: 50 }, // Giữ 50 users trong 1 phút
{ duration: "30s", target: 100 }, // Peak 100 users
{ duration: "30s", target: 0 }, // Giảm về 0
],
thresholds: {
http_req_duration: ["p(95)<500"], // 95% requests < 500ms
http_req_failed: ["rate<0.01"], // Tỷ lệ lỗi < 1%
},
};
export default function () {
// Trang chủ
const homeRes = http.get("http://localhost:3000");
check(homeRes, {
"trang chủ status 200": (r) => r.status === 200,
"trang chủ load < 1s": (r) => r.timings.duration < 1000,
});
// API sản phẩm
const productsRes = http.get("http://localhost:3000/api/products?page=1");
check(productsRes, {
"API products status 200": (r) => r.status === 200,
"API products load < 500ms": (r) => r.timings.duration < 500,
});
sleep(1);
}
# Chạy load test
k6 run load-tests/api-test.js
# Với output HTML report
k6 run --out json=results.json load-tests/api-test.js
Kiểm tra bảo mật cho API routes:
1. SQL Injection — Thử inject qua query params và body
2. XSS — Kiểm tra output encoding
3. CSRF — Kiểm tra CSRF protection
4. Authentication bypass — Thử truy cập không có token
5. Authorization — Thử truy cập resource của user khác
6. Rate limiting — Kiểm tra brute force protection
7. File upload — Kiểm tra file type validation
8. IDOR — Thử truy cập bằng ID của user khác
Tạo test cases cho từng loại vulnerability.
// __tests__/security/xss.test.ts
describe("XSS Prevention", () => {
it("escape HTML trong user input", async () => {
const maliciousInput = '<script>alert("xss")</script>';
const res = await request(app)
.post("/api/products")
.send({ name: maliciousInput, price: 100 });
// Kiểm tra dữ liệu được sanitize
expect(res.body.name).not.toContain("<script>");
});
});
// __tests__/security/sql-injection.test.ts
describe("SQL Injection Prevention", () => {
it("không bị SQL injection qua search param", async () => {
const maliciousQuery = "'; DROP TABLE users; --";
const res = await request(app)
.get(`/api/products?search=${encodeURIComponent(maliciousQuery)}`);
expect(res.status).not.toBe(500);
// Verify bảng users vẫn tồn tại
const users = await prisma.user.count();
expect(users).toBeGreaterThan(0);
});
});
Áp dụng TDD cho chức năng "Thêm sản phẩm vào giỏ hàng":
Bước 1: Viết test trước (RED)
- Test thêm sản phẩm mới vào giỏ
- Test tăng số lượng nếu đã có trong giỏ
- Test giới hạn số lượng theo stock
- Test tính tổng tiền
Bước 2: Viết code tối thiểu để pass (GREEN)
Bước 3: Refactor
Hãy bắt đầu với bước 1 — viết tất cả test cases.
/engineering-skills:tdd-guide/engineering-skills:tdd-guide
Tôi cần implement tính năng discount coupons:
- Mã giảm giá theo phần trăm hoặc số tiền cố định
- Giới hạn số lần sử dụng
- Thời hạn hiệu lực
- Áp dụng cho toàn đơn hoặc sản phẩm cụ thể
- Giá trị đơn hàng tối thiểu
Hãy hướng dẫn tôi từng bước theo TDD.
/engineering-skills:senior-qa/engineering-skills:senior-qa
Review test suite hiện tại của dự án và đề xuất:
1. Các test case còn thiếu
2. Edge cases chưa được cover
3. Test code cần refactor
4. Chiến lược test cho tính năng mới
5. Cải thiện test coverage
Tạo QA test plan cho sprint release:
Tính năng mới:
1. Hệ thống coupon giảm giá
2. Đánh giá sản phẩm (1-5 sao + bình luận)
3. Thông báo đơn hàng qua email
Với mỗi tính năng, cần:
- Test cases (happy path + edge cases)
- Test data preparation
- Acceptance criteria
- Regression test scope
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run test -- --coverage
- uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
e2e-test:
runs-on: ubuntu-latest
needs: unit-test
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db
- run: npx playwright install --with-deps
- run: npx playwright test
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test_db
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
# .github/workflows/go-test.yml
name: Go Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go test -v -race -coverprofile=coverage.out ./...
- run: go tool cover -func=coverage.out
Tạo test suite hoàn chỉnh cho module quản lý đơn hàng:
Service: OrderService
- createOrder(userId, items[], shippingAddress)
- getOrderById(orderId)
- listOrders(userId, filters)
- updateOrderStatus(orderId, status)
- cancelOrder(orderId, reason)
- calculateShipping(address, items)
Viết:
1. Unit tests cho mỗi method (happy path + error cases)
2. Integration test cho flow tạo đơn hàng
3. Mock database calls với Jest
4. Test data factories
5. Đạt coverage > 90%
Test E2E "đặt hàng thành công" bị flaky (pass/fail ngẫu nhiên).
Triệu chứng:
- Pass khi chạy riêng, fail khi chạy cùng suite
- Timeout ở bước chờ redirect sau thanh toán
- Lỗi: "Element not found: .order-confirmation"
Hãy phân tích nguyên nhân và đề xuất cách fix.
Đây là function cũ không có test. Hãy:
1. Phân tích function và xác định các test cases cần viết
2. Refactor nếu cần để dễ test hơn
3. Viết unit tests đầy đủ
4. Đề xuất cải thiện code
[dán code vào đây]
Gợi ý tiếp theo: