📕 Phần 4 · Nâng Cao · Chương 9

Best Practices & Testing

Nâng tầm code từ "chạy được" lên "production-ready" — với code quality tools, unit testing, documentation đúng chuẩn và Git workflow chuyên nghiệp.

7 giờ học
📝 7 bài học
Test + CI/CD tự động hóa
📊 Mức độ: Nâng cao
✨ 1. ESLint + Prettier
Code quality & formatting
🧪 2. Unit Testing
Vitest, mocking, TDD
📄 3. Documentation
JSDoc, README chuẩn, Swagger
🌿 4. Git Chuyên Nghiệp
Branch strategy, PR, commit
🔬 5. Testing Hoàn Chỉnh
Unit + Integration + CI/CD
🤖 6. AI Code Review
Refactor & review với Copilot
⚡ 7. GitHub Actions CI/CD
Tự động lint+test+deploy

🎯 Mục tiêu học tập

  • Setup ESLint + Prettier để code nhất quán và không lỗi
  • Viết unit tests với Vitest và pytest — mocking, coverage, TDD
  • Viết README chuyên nghiệp và JSDoc/Swagger documentation
  • Áp dụng Git workflow với Conventional Commits và branching strategy
  • Xây dựng GitHub Actions CI/CD pipeline: tự động lint → test → deploy với PostgreSQL service
1

Bài 9.1 — Code Quality: ESLint + Prettier

ESLint
Static analysis tool bắt lỗi và anti-patterns trong code JavaScript/TypeScript không cần chạy. Đặc biệt quan trọng với AI-generated code.
Prettier
Opinionated code formatter — thiết lập style (indent, quotes, dongs...) một lần, áp dụng toàn team tự động. Không còn tranh cãi code style.
Pre-commit Hook
Script chạy tự động trước mỗi git commit. Dug Husky để chạy ESLint+Prettier. Nếu fail, commit bị từ chối — bảo vệ có hiệu quả.
Test Coverage
Phần trăm các dòng code được chạy bởi tests. Coverage 80%+ là mục tiêu thực tế cho business logic. Không cần 100%.
TDD (Test-Driven Development)
Viết test trước, code sau: Red (test fail) → Green (code pass) → Refactor. Tests là specification của behavior, không chỉ là safety net.
Mocking
Thay thế dependency thật bằng version giả trong tests. Mock database, API calls, file system — giúp tests chạy nhanh và deterministic.

Tại sao cần Linter và Formatter?

ESLint — Phát hiện lỗi

Tìm bugs tiềm năng, unused variables, anti-patterns trước khi runtime. Đặc biệt quan trọng với AI-generated code.

Prettier — Format code

Tự động format code: indent, quotes, trailing commas... Không cần tranh luận style, mọi người code như nhau.

Pre-commit Hooks

Tự động chạy ESLint + Prettier trước mỗi commit với Husky. Code xấu sẽ không được commit.

Setup ESLint + Prettier cho dự án Node.js/React

bash
npm install -D eslint prettier eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks

# Init ESLint config
npx eslint --init
javascript — .eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    es2022:  true,
    node:    true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier', // Tắt các rules conflict với Prettier — phải để cuối
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType:  'module',
    ecmaFeatures: { jsx: true },
  },
  rules: {
    'no-unused-vars':    ['warn', { argsIgnorePattern: '^_' }],
    'no-console':        ['warn', { allow: ['warn', 'error'] }],
    'react/prop-types':  'warn',
    'react/react-in-jsx-scope': 'off', // React 17+ không cần import React
  },
  settings: {
    react: { version: 'detect' },
  },
};
javascript — .prettierrc
{
  "semi":          true,
  "singleQuote":   true,
  "tabWidth":      2,
  "trailingComma": "es5",
  "printWidth":    100,
  "arrowParens":   "avoid"
}

Setup Husky — Pre-commit Hooks

bash
npm install -D husky lint-staged
npx husky install

# Add pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
javascript — package.json (thêm phần lint-staged)
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css}": [
      "prettier --write"
    ]
  }
}
💡
AI + ESLint = Code review tự động

Khi AI generate code và ESLint báo lỗi, paste lỗi vào Copilot Chat: "ESLint báo lỗi này: [error message]. Fix cho tôi và giải thích tại sao?" — Cách học tốt nhất là sửa lỗi thực tế.


2

Bài 9.2 — Unit Testing

Quy Trình TDD — Red, Green, Refactor

🔴
RED — Viết test thất bại trước

Viết test mô tả behavior mong muốn TRƯỚC KHI có code. Test phải fail vì chưa implement. Đây là lúc define "done" rõ ràng nhất.

🟢
GREEN — Viết code tối thiểu để pass

Viết CODE ÍT NHẤT để test pass. Không cần đẹp, không cần tối ưu. Mục tiêu duy nhất: test xanh.

🔵
REFACTOR — Cải thiện code

Refactor code sạch hơn, loại bỏ duplication, tối ưu. Tests vẫn phải pass sau khi refactor. Đây là "safety net".

Jest / Vitest Matchers — Tham Khảo Nhanh

MatcherDùng choVí dụ
toBe(val)So sánh bằng (===)expect(2+2).toBe(4)
toEqual(obj)So sánh deep equality (object/array)expect(user).toEqual({id: 1, name: 'An'})
toBeNull()Kiểm tra nullexpect(result).toBeNull()
toBeTruthy() / toBeFalsy()Truthy / Falsy checkexpect(token).toBeTruthy()
toContain(item)Array/string chứa itemexpect(list).toContain('react')
toThrow(msg?)Function ném lỗiexpect(() => fn()).toThrow('Invalid')
toHaveBeenCalled()Mock function đã được gọiexpect(mockFn).toHaveBeenCalledTimes(1)
toMatchObject(obj)Object chứa subsetexpect(response).toMatchObject({status: 200})
resolves / rejectsPromise resolved/rejectedawait expect(fn()).resolves.toBe('ok')

JavaScript: Testing với Jest + Vitest

bash
# Cho Vite/React project — dùng Vitest (nhanh hơn Jest, tích hợp Vite)
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom

# Cho Node.js — dùng Jest
npm install -D jest @jest/globals supertest
javascript — utils/formatPrice.test.js
import { describe, it, expect } from 'vitest';
import { formatPrice, formatDate } from './formatters';

describe('formatPrice', () => {
  it('formats number to VND currency', () => {
    expect(formatPrice(100000)).toBe('100.000 ₫');
    expect(formatPrice(1500000)).toBe('1.500.000 ₫');
  });

  it('returns "0 ₫" for 0', () => {
    expect(formatPrice(0)).toBe('0 ₫');
  });

  it('handles negative numbers', () => {
    expect(formatPrice(-5000)).toBe('-5.000 ₫');
  });
});

describe('formatDate', () => {
  it('formats ISO date to dd/MM/yyyy', () => {
    expect(formatDate('2024-01-15')).toBe('15/01/2024');
  });
});
javascript — API Integration Test với Supertest
const request = require('supertest');
const app = require('../src/app');
const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();
let authToken;

beforeAll(async () => {
  // Tạo user test và lấy token
  const res = await request(app)
    .post('/api/auth/register')
    .send({ email: 'test@test.com', username: 'testuser', password: 'Password123' });
  
  authToken = res.body.token;
});

afterAll(async () => {
  // Dọn sạch test data
  await prisma.user.deleteMany({ where: { email: 'test@test.com' } });
  await prisma.$disconnect();
});

describe('POST /api/todos', () => {
  it('creates a todo when authenticated', async () => {
    const res = await request(app)
      .post('/api/todos')
      .set('Authorization', `Bearer ${authToken}`)
      .send({ title: 'Test todo', priority: 'high' });

    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({
      title:     'Test todo',
      priority:  'high',
      completed: false,
    });
  });

  it('returns 401 without auth token', async () => {
    const res = await request(app).post('/api/todos').send({ title: 'Test' });
    expect(res.status).toBe(401);
  });

  it('returns 400 for empty title', async () => {
    const res = await request(app)
      .post('/api/todos')
      .set('Authorization', `Bearer ${authToken}`)
      .send({ title: '' });

    expect(res.status).toBe(400);
  });
});

Python: Testing với pytest

python — tests/test_organizer.py
import pytest
from pathlib import Path
from organizer import categorize_file, organize_directory

# Dùng tmp_path fixture của pytest (tự tạo và xóa folder tạm)
def test_categorize_image(tmp_path):
    file = tmp_path / "photo.jpg"
    assert categorize_file(file) == "Images"

def test_categorize_document(tmp_path):
    file = tmp_path / "report.pdf"
    assert categorize_file(file) == "Documents"

def test_categorize_unknown(tmp_path):
    file = tmp_path / "data.xyz"
    assert categorize_file(file) == "Others"

def test_organize_moves_files(tmp_path):
    # Tạo files test
    (tmp_path / "photo.jpg").touch()
    (tmp_path / "doc.pdf").touch()
    (tmp_path / "music.mp3").touch()
    
    organize_directory(tmp_path)
    
    # Kiểm tra files đã di chuyển đúng chỗ
    assert (tmp_path / "Images"    / "photo.jpg").exists()
    assert (tmp_path / "Documents" / "doc.pdf").exists()
    assert (tmp_path / "Audio"     / "music.mp3").exists()

def test_organize_handles_empty_dir(tmp_path):
    # Không nên raise exception với folder rỗng
    organize_directory(tmp_path)
    assert True  # Reached without exception

Prompt AI để generate tests

text — Prompt mẫu
Đây là function tôi cần test:
[paste function code]

Viết unit tests với Jest/Vitest cho function này:
- Happy path (input hợp lệ)
- Edge cases (empty, null, boundary values)
- Error cases (invalid input)
Đặt mỗi test case trong describe/it rõ ràng

3

Bài 9.3 — Documentation Chuyên Nghiệp

README chuẩn dự án

markdown — README.md template
# Tên Dự Án

> Một dòng mô tả ngắn gọn và hấp dẫn về dự án.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org)

## ✨ Tính năng

- ✅ Authentication (JWT)
- ✅ CRUD operations
- ✅ File upload
- 🚧 Email notifications (coming soon)

## 🚀 Bắt đầu nhanh

### Yêu cầu hệ thống
- Node.js 18+
- npm 9+

### Cài đặt

```bash
git clone https://github.com/your-username/project-name.git
cd project-name
npm install
cp .env.example .env   # Điền thông tin cấu hình
npm run dev
```

Truy cập: http://localhost:3000

## ⚙️ Cấu hình (.env)

| Biến                | Mô tả                     | Mặc định   |
|---------------------|---------------------------|------------|
| `PORT`              | Port server                | `3000`     |
| `DATABASE_URL`      | Chuỗi kết nối database     | Bắt buộc   |
| `JWT_SECRET`        | Secret key cho JWT         | Bắt buộc   |
| `ALLOWED_ORIGIN`    | CORS allowed origin        | `http://localhost:5173` |

## 📡 API Endpoints

| Method | Endpoint           | Mô tả              | Auth |
|--------|--------------------|--------------------|------|
| POST   | `/api/auth/register` | Đăng ký tài khoản | ❌   |
| POST   | `/api/auth/login`    | Đăng nhập          | ❌   |
| GET    | `/api/todos`         | Lấy danh sách todo | ✅   |
| POST   | `/api/todos`         | Tạo todo mới       | ✅   |

## 🧪 Chạy Tests

```bash
npm test              # Chạy tất cả tests
npm run test:watch    # Watch mode
npm run test:coverage # Xem test coverage
```

## 🤝 Đóng góp

Pull requests được chào đón. Xem [CONTRIBUTING.md](CONTRIBUTING.md) để biết thêm.

## 📝 License

MIT © [Tên của bạn]

4

Bài 9.4 — Git Workflow Chuyên Nghiệp

Conventional Commits

Conventional Commits là quy ước đặt tên commit message theo format chuẩn. AI rất giỏi generate commit messages chuẩn.

text — Format
type(scope): description

[optional body]

[optional footer]

------- CÁC TYPES -------
feat:     Thêm tính năng mới
fix:      Sửa bug
docs:     Thay đổi documentation
style:    Format code (không đổi logic)
refactor: Refactor code (không feat, không fix)
test:     Thêm hoặc sửa tests
chore:    Cập nhật build scripts, dependencies

------- VÍ DỤ -------
feat(auth): add JWT refresh token endpoint
fix(api): return 404 when post not found
docs(readme): add API endpoints table
test(todos): add edge cases for empty title
chore(deps): upgrade prisma to v5.8

Branching Strategy (GitHub Flow)

bash
# Main branch = production-ready, luôn deployable
# Mỗi feature/fix = 1 branch riêng

# 1. Tạo feature branch từ main
git checkout main
git pull origin main
git checkout -b feat/add-search-filter

# 2. Code, commit thường xuyên
git add -p                         # Chọn từng hunk để add
git commit -m "feat(posts): add search by title"

# 3. Push và tạo Pull Request
git push -u origin feat/add-search-filter
# → Tạo PR trên GitHub → Code review → Merge

# 4. Sau khi merge, xóa branch
git checkout main
git pull origin main
git branch -d feat/add-search-filter

Dùng AI để viết commit messages

bash
# Xem diff trước khi commit
git diff --staged

# Nhờ Copilot CLI generate commit message
# (Copilot sẽ đọc diff và suggest message chuẩn Conventional Commits)
git commit   # Copilot sẽ suggest message trong terminal
💡
GitHub Copilot CLI

Cài gh copilot extension: gh extension install github/gh-copilot. Sau đó dùng gh copilot suggest để nhờ AI suggest git commands, hoặc gh copilot explain để AI giải thích một lệnh phức tạp.


5

Bài 9.5 — Setup Testing Hoàn Chỉnh: Unit Test + Integration Test + CI/CD

Chúng ta sẽ setup hệ thống testing đầy đủ cho cả backend (Vitest + Supertest) và frontend (React Testing Library), sau đó tích hợp vào CI/CD pipeline với GitHub Actions.

Bước 1 — Setup Testing Cho Backend (Express API)

bash — Cài testing dependencies cho backend
cd backend

# Cài Vitest (nhanh hơn Jest, tương thích Jest API)
npm install --save-dev vitest supertest @vitest/coverage-v8

# Thêm script vào package.json
# "test": "vitest run"
# "test:watch": "vitest"
# "test:coverage": "vitest run --coverage"

# Tạo thư mục test
mkdir tests tests/unit tests/integration
touch tests/unit/utils.test.js tests/integration/auth.test.js tests/integration/notes.test.js
javascript — tests/unit/utils.test.js
// tests/unit/utils.test.js — Unit test cho helper functions
// Prompt Copilot: "Viết unit tests cho các functions trong utils.js"
import { describe, it, expect } from 'vitest';

// Ví dụ: test helper function validateEmail
function validateEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validatePassword(password) {
  return password.length >= 8;
}

describe('validateEmail', () => {
  it('trả về true với email hợp lệ', () => {
    expect(validateEmail('user@example.com')).toBe(true);
    expect(validateEmail('user+tag@domain.co')).toBe(true);
  });

  it('trả về false với email không hợp lệ', () => {
    expect(validateEmail('notanemail')).toBe(false);
    expect(validateEmail('@domain.com')).toBe(false);
    expect(validateEmail('')).toBe(false);
  });
});

describe('validatePassword', () => {
  it('chấp nhận password đủ 8 ký tự', () => {
    expect(validatePassword('12345678')).toBe(true);
    expect(validatePassword('LongPassword123')).toBe(true);
  });

  it('từ chối password ngắn hơn 8 ký tự', () => {
    expect(validatePassword('short')).toBe(false);
    expect(validatePassword('')).toBe(false);
  });
});
javascript — tests/integration/auth.test.js
// tests/integration/auth.test.js — Integration test cho auth endpoints
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import app from '../../src/app.js'; // Export app riêng (không gọi app.listen)
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Cleanup database trước và sau test suite
beforeAll(async () => {
  await prisma.user.deleteMany({ where: { email: { contains: '@test.com' } } });
});

afterAll(async () => {
  await prisma.user.deleteMany({ where: { email: { contains: '@test.com' } } });
  await prisma.$disconnect();
});

describe('POST /api/auth/register', () => {
  it('đăng ký thành công với thông tin hợp lệ', async () => {
    const res = await request(app)
      .post('/api/auth/register')
      .send({ email: 'newuser@test.com', password: 'Password123', name: 'Test User' });

    expect(res.status).toBe(201);
    expect(res.body).toHaveProperty('token');
    expect(res.body.user.email).toBe('newuser@test.com');
    expect(res.body.user).not.toHaveProperty('password'); // Không trả về password
  });

  it('trả về 409 khi email đã tồn tại', async () => {
    // Đăng ký lần 2 với cùng email
    const res = await request(app)
      .post('/api/auth/register')
      .send({ email: 'newuser@test.com', password: 'Password123' });

    expect(res.status).toBe(409);
    expect(res.body.error).toMatch(/đã được đăng ký/);
  });
});

describe('POST /api/auth/login', () => {
  it('đăng nhập thành công với thông tin đúng', async () => {
    const res = await request(app)
      .post('/api/auth/login')
      .send({ email: 'newuser@test.com', password: 'Password123' });

    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty('token');
  });

  it('trả về 401 với mật khẩu sai', async () => {
    const res = await request(app)
      .post('/api/auth/login')
      .send({ email: 'newuser@test.com', password: 'WrongPassword' });

    expect(res.status).toBe(401);
  });
});
bash — Chạy tests
# Chạy tất cả tests một lần
npm test

# Chạy tests ở chế độ watch (tự reload khi save file)
npm run test:watch

# Chạy tests với coverage report
npm run test:coverage

# Chạy chỉ một file test cụ thể
npx vitest run tests/integration/auth.test.js

# Chạy tests match pattern tên
npx vitest run --reporter=verbose

Bước 2 — Setup ESLint + Prettier

bash — Cài và cấu hình ESLint
# Cài ESLint (chạy trong thư mục gốc project)
npm install --save-dev eslint @eslint/js

# Khởi tạo config ESLint
npx eslint --init
# Trả lời các câu hỏi:
# - To check syntax and find problems
# - CommonJS (hoặc ESM tùy project)
# - None of these (nếu không dùng framework)
# - No (TypeScript)
# - Node.js
# - JSON format

# Cài Prettier
npm install --save-dev prettier eslint-config-prettier

# Tạo .prettierrc
echo '{"semi":true,"singleQuote":true,"tabWidth":2,"printWidth":100}' > .prettierrc

# Tạo .eslintignore
echo 'node_modules\ndist\ncoverage' > .eslintignore
bash — Cài Husky (pre-commit hooks)
# Cài Husky và lint-staged
npm install --save-dev husky lint-staged

# Khởi tạo Husky
npx husky init

# Tạo pre-commit hook
echo 'npx lint-staged' > .husky/pre-commit

# Thêm lint-staged config vào package.json:
# "lint-staged": {
#   "*.js": ["eslint --fix", "prettier --write"],
#   "*.{json,md}": ["prettier --write"]
# }

# Test pre-commit hook
git add .
git commit -m "test: add testing setup"
# Hook sẽ tự động chạy ESLint + Prettier trước khi commit

Bước 3 — Setup GitHub Actions CI/CD

bash — Tạo file CI workflow
mkdir -p .github/workflows
touch .github/workflows/ci.yml
yaml — .github/workflows/ci.yml
name: CI — Lint, Test, Build

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test-backend:
    name: Backend Tests
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: backend/package-lock.json

      - name: Install backend dependencies
        working-directory: ./backend
        run: npm ci

      - name: Run ESLint
        working-directory: ./backend
        run: npm run lint

      - name: Run tests
        working-directory: ./backend
        env:
          DATABASE_URL: "file:./test.db"
          JWT_SECRET: "ci-test-secret-key-32-characters-long"
          NODE_ENV: "test"
        run: npm test

  test-frontend:
    name: Frontend Build Check
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install frontend dependencies
        working-directory: ./frontend
        run: npm ci

      - name: Build frontend
        working-directory: ./frontend
        run: npm run build
CI/CD Pipeline Hoạt Động!

Mỗi khi bạn push code lên GitHub, workflow sẽ tự động chạy: lint → test → build. Pull Request sẽ bị block nếu có test fail. Đây là quy trình chuyên nghiệp mà mọi team làm việc.

💡 Mẹo từ ThanhDoIT
  • Test names nên đọc như câu chuyện: "given X, when Y, then Z". Ví dụ: "given valid email, when user registers, then returns 201 with token".
  • Đừng mock quá nhiều trong tests — mock chỉ external services (APIs, email). Test với real database (SQLite in-memory) cho integration tests.
  • Commit message quan trọng hơn bạn nghĩ. Sau 6 tháng, git log là cách duy nhất để hiểu tại sao code được viết như vậy.
  • Husky + lint-staged = code review tự động. Mỗi commit đều pass linter — team không cần nói "nhớ chạy eslint" nữa.

6

Bài 9.6 — AI-Assisted Code Review & Refactoring

Code review là kỹ năng quan trọng nhất để trở thành developer giỏi. AI có thể review code của bạn như một senior developer — bắt bugs, suggest improvements, và giải thích tại sao. Đây là bài học về cách dùng AI để nâng cao chất lượng code liên tục.

🔍
Code Review Dimensions
  • Correctness: logic có đúng không?
  • Security: có vulnerabilities không?
  • Performance: bottlenecks, N+1 queries
  • Maintainability: dễ đọc, dễ thay đổi
♻️
Refactoring Patterns
  • Extract function / Extract class
  • Replace magic numbers with constants
  • Eliminate duplication (DRY)
  • Simplify conditionals

Prompt Template: Code Review Toàn Diện

Hãy review code này theo vai trò Senior Developer với 10 năm kinh nghiệm.
Phân tích theo các dimension sau:

**1. CORRECTNESS**
- Logic có đúng không? Có edge cases nào bị bỏ qua?
- Error handling có đầy đủ không?
- Race conditions hay async issues?

**2. SECURITY** (OWASP Top 10)
- Input validation đủ chưa?
- Data exposure risks?
- Authentication/Authorization checks?

**3. PERFORMANCE**
- Có N+1 query problem không? (database)
- Unnecessary re-renders? (React)
- Memory leaks tiềm ẩn?
- Có thể cache gì?

**4. MAINTAINABILITY**
- Function/class có làm quá nhiều việc? (Single Responsibility)
- Có magic numbers/strings cần extract thành constants?
- Naming: variable names có rõ ràng không?
- Duplication: có code nào cần refactor DRY?

**5. TESTS**
- Cần viết thêm tests cho trường hợp nào?
- Test coverage hiện tại có đủ không?

Code cần review:
[paste code vào đây]

Format output: từng issue với severity (critical/major/minor) + code fix cụ thể.
Framework review 5 chiều này giúp Copilot cung cấp feedback có cấu trúc thay vì góp ý lan man.

Refactoring Thực Tế: Before & After

typescript — BEFORE: Code xấu cần refactor
// ❌ BEFORE: Nhiều vấn đề
async function processOrder(o: any) {
  if (o.status == 'pending') {
    const db = await getDb();
    const u = await db.query(`SELECT * FROM users WHERE id = ${o.user_id}`); // SQL injection!
    if (u.rows.length > 0) {
      if (u.rows[0].balance >= o.amount) {
        await db.query(`UPDATE users SET balance = balance - ${o.amount} WHERE id = ${o.user_id}`);
        await db.query(`UPDATE orders SET status = 'completed' WHERE id = ${o.id}`);
        // Gửi email nhưng không handle error
        sendEmail(u.rows[0].email, 'Order completed');
        return true;
      } else {
        return false;
      }
    }
  }
  return false;
}
typescript — AFTER: Refactored với AI
// ✅ AFTER: Clean, safe, maintainable
interface Order { id: number; userId: number; amount: number; status: OrderStatus; }
type OrderStatus = 'pending' | 'completed' | 'failed' | 'cancelled';

class InsufficientBalanceError extends Error {
  constructor() { super('Insufficient balance'); this.name = 'InsufficientBalanceError'; }
}

async function processOrder(order: Order): Promise<void> {
  if (order.status !== 'pending') {
    throw new Error(`Cannot process order with status: ${order.status}`);
  }

  const user = await prisma.user.findUniqueOrThrow({ where: { id: order.userId } });

  if (user.balance < order.amount) {
    throw new InsufficientBalanceError();
  }

  // Transaction: cả hai update xảy ra cùng lúc hoặc không cái nào
  await prisma.$transaction([
    prisma.user.update({
      where: { id: order.userId },
      data: { balance: { decrement: order.amount } }
    }),
    prisma.order.update({
      where: { id: order.id },
      data: { status: 'completed' }
    })
  ]);

  // Không block order process nếu email fail
  sendOrderConfirmationEmail(user.email, order).catch(err => {
    console.error('Failed to send confirmation email:', err);
    // Log to monitoring service but don't throw
  });
}
Refactor đoạn code này cho tôi theo các nguyên tắc:
- Single Responsibility: mỗi function chỉ làm 1 việc
- DRY: loại bỏ code trùng lặp
- Early return: reduce nesting levels
- TypeScript: thêm types đầy đủ, không dùng any
- Error handling: sử dụng custom errors thay vì return boolean
- Security: fix SQL injection nếu có
- Database: wrap multiple writes trong transaction

[paste code vào đây]

Sau khi refactor, giải thích từng thay đổi và tại sao nó tốt hơn.
Liệt kê cụ thể các nguyên tắc refactoring → Copilot apply đúng theo checklist, không bỏ sót.

Code Metrics — Đo Lường Chất Lượng Code

MetricTargetTool
Test Coverage> 70% cho utils, > 50% tổng thểJest --coverage
Cyclomatic Complexity< 10 per functionESLint complexity rule
Function Length< 30 lines (guideline)ESLint max-lines-per-function
Bundle Size (Frontend)< 200KB initial JSvite-bundle-analyzer
Lighthouse Score> 90 PerformanceChrome DevTools
TypeScript Strict0 type errorstsc --noEmit
Dependency AgeNo critical CVEsnpm audit
💡 Workflow Code Review Hàng Ngày
  • Trước commit: Highlight code mới viết → Copilot /review → fix issues
  • Khi viết PR: Paste diff vào Copilot Chat → "Review PR này, tìm bugs và suggest improvements"
  • Học từ AI: Sau khi nhận feedback, hỏi thêm "Tại sao approach này tốt hơn? Nguyên tắc design pattern nào?"
  • Weekly: Chạy npm audit + update dependencies — hỏi Copilot về breaking changes
🎯 Thực Hành: Self Code Review Session
  1. Lấy file code phức tạp nhất trong project của bạn (>50 lines)
  2. Mở Copilot Chat, paste code + prompt review 5 chiều ở trên
  3. Với từng issue Copilot tìm thấy: hiểu tại sao, rồi fix
  4. Refactor 1 function dùng prompt template refactoring
  5. Chạy npm test -- --coverage → xem coverage report, viết thêm tests cho functions coverage thấp
  6. So sánh code trước và sau refactoring — cảm nhận sự khác biệt
🎯 Bài Tập Tổng Kết Chương 9 — Quality Audit
  1. Setup ESLint + Prettier + Husky cho project của bạn — từ giờ mỗi commit đều qua linter
  2. Viết unit tests cho 5 functions quan trọng nhất — target 70% coverage
  3. Dùng prompt review 5 chiều để review lại toàn bộ auth module
  4. Refactor 2 functions phức tạp nhất theo nguyên tắc Single Responsibility
  5. Viết README hoàn chỉnh với badges (CI status, coverage, license)
  6. Thử thách: Setup SonarCloud (free cho open source) — scan toàn bộ codebase và fix tất cả issues Critical/Major
⚠️ 5 Cạm Bẫy Phổ Biến Trong Testing & Code Quality
  • Test chỉ happy path: 90% bugs xảy ra ở edge cases — empty input, null, concurrent requests. Copilot prompt: “Generate edge case tests cho function này”
  • Mock quá nhiều: Unit test mock mọi thứ → test pass nhưng production fail. Kết hợp integration tests với real dependencies để đảm bảo end-to-end.
  • Test coverage ≠ code quality: 100% coverage với tests trivial vẫn là code xấu. Focus vào test meaningful behaviors, không chase số %.
  • Bỏ qua ESLint warnings: Warnings hôm nay = bugs ngày mai. Treat warnings as errors trong CI: thêm "error": true trong ESLint config.

7

Bài 9.7 — GitHub Actions CI/CD: Tự Động Hóa Toàn Bộ Pipeline

CI/CD (Continuous Integration / Continuous Deployment) là nền tảng của mọi team phát triển phần mềm chuyên nghiệp. GitHub Actions cho phép bạn thiết lập pipeline tự động: mỗi lần push code → chạy lint → chạy tests → build → deploy. Hoàn toàn miễn phí cho public repos và 2000 phút/tháng cho private repos.

CI (Continuous Integration)
Mỗi lần push code → tự động chạy lint + tests. Phát hiện lỗi ngay lập tức, trước khi merge vào main.
CD (Continuous Deployment)
Sau khi CI pass → tự động deploy lên server. Code merge vào main = live trên production trong vài phút.
GitHub Actions Workflow
File YAML trong .github/workflows/ định nghĩa "khi nào" chạy "gì". Mỗi file = 1 workflow.
GitHub Secrets
Biến môi trường mã hóa, lưu trong repository settings. Dùng cho API keys, deployment credentials — không bao giờ hardcode.

1. CI Workflow — Lint & Test tự động

.github/workflows/ci.yml — Chạy khi push hoặc pull request
name: CI — Lint & Test

# Trigger: chạy khi push vào bất kỳ branch, hoặc khi tạo Pull Request
on:
  push:
    branches: ['**']
  pull_request:
    branches: [main, develop]

jobs:
  lint-and-test:
    name: Lint & Test (Node ${{ matrix.node-version }})
    runs-on: ubuntu-latest

    # Test trên nhiều Node.js versions để đảm bảo tương thích
    strategy:
      matrix:
        node-version: [20.x, 22.x]
      fail-fast: false  # Tiếp tục test version khác nếu 1 version fail

    steps:
      # Bước 1: Checkout code từ repo
      - name: Checkout repository
        uses: actions/checkout@v4

      # Bước 2: Cài Node.js (dùng version từ matrix)
      - name: Set up Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'  # Cache node_modules để build nhanh hơn

      # Bước 3: Cài dependencies (ci = clean install, không tự cập nhật lockfile)
      - name: Install dependencies
        run: npm ci

      # Bước 4: Chạy ESLint — fail nếu có lỗi
      - name: Run ESLint
        run: npm run lint

      # Bước 5: Kiểm tra format Prettier (không sửa file, chỉ kiểm tra)
      - name: Check Prettier formatting
        run: npm run format:check
        # Thêm vào package.json: "format:check": "prettier --check ."

      # Bước 6: Build TypeScript (phát hiện type errors)
      - name: Build TypeScript
        run: npm run build

      # Bước 7: Chạy tests với coverage report
      - name: Run tests with coverage
        run: npm run test:coverage
        # Thêm vào package.json: "test:coverage": "vitest run --coverage"
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
          JWT_ACCESS_SECRET: test-secret-minimum-32-characters-long
          NODE_ENV: test

    # Dịch vụ phụ: PostgreSQL cho integration tests
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      # Bước 8: Chạy Prisma migrations trong test database
      - name: Run database migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db

      # Bước 9: Upload coverage report lên Codecov (optional)
      - name: Upload coverage to Codecov
        if: matrix.node-version == '22.x'
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}  # Optional, public repos không cần

2. CD Workflow — Deploy tự động lên production

.github/workflows/deploy.yml — Deploy khi merge vào main
name: Deploy to Production

# Chỉ deploy khi CI đã pass trên branch main
on:
  push:
    branches: [main]

# Chỉ chạy 1 deploy cùng lúc — tránh race condition
concurrency:
  group: production-deploy
  cancel-in-progress: false

jobs:
  # Job 1: Chạy CI trước (reuse workflow)
  ci:
    uses: ./.github/workflows/ci.yml

  # Job 2: Deploy — chỉ chạy nếu CI pass
  deploy:
    name: Deploy to Railway
    runs-on: ubuntu-latest
    needs: [ci]  # Phụ thuộc CI job phải pass
    environment:
      name: production
      url: https://myapp.up.railway.app

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22.x'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci --production=false

      - name: Build application
        run: npm run build
        env:
          NODE_ENV: production

      # === OPTION A: Deploy lên Railway ===
      - name: Deploy to Railway
        uses: bervProject/railway-deploy@main
        with:
          railway_token: ${{ secrets.RAILWAY_TOKEN }}
          service: ${{ secrets.RAILWAY_SERVICE_ID }}

      # === OPTION B: Deploy lên Render (chọn 1 trong A/B/C) ===
      # - name: Deploy to Render
      #   run: |
      #     curl -X POST "${{ secrets.RENDER_DEPLOY_HOOK_URL }}"

      # === OPTION C: Deploy lên VPS với SSH ===
      # - name: Deploy to VPS via SSH
      #   uses: appleboy/ssh-action@v1
      #   with:
      #     host: ${{ secrets.VPS_HOST }}
      #     username: ${{ secrets.VPS_USER }}
      #     key: ${{ secrets.VPS_SSH_KEY }}
      #     script: |
      #       cd /var/www/myapp
      #       git pull origin main
      #       npm ci --production
      #       npm run build
      #       pm2 restart myapp

      - name: Notify deployment success
        if: success()
        run: echo "✅ Deployed successfully to production!"

3. Cấu Hình GitHub Secrets

Secret NameGiá TrịDùng Cho
DATABASE_URLPostgreSQL connection string productionPrisma migrate trong CD
JWT_ACCESS_SECRETChuỗi ngẫu nhiên ≥32 ký tựJWT signing trong tests/prod
RAILWAY_TOKENToken từ Railway dashboardDeploy Railway
RAILWAY_SERVICE_IDService ID từ RailwayXác định service cần deploy
VPS_HOSTIP hoặc domain VPSSSH deploy tới VPS
VPS_SSH_KEYPrivate key SSH (RSA/Ed25519)Xác thực SSH không cần password
CODECOV_TOKENToken từ codecov.ioUpload coverage report
1
Tạo thư mục workflow

Chạy: mkdir -p .github/workflows. Tạo 2 file ci.ymldeploy.yml theo template trên.

2
Cập nhật package.json scripts

Đảm bảo có: "lint", "format:check", "build", "test:coverage" scripts hoạt động đúng.

3
Thêm Secrets vào GitHub

Vào Repository → Settings → Secrets and variables → Actions. Thêm từng secret theo bảng trên. Không bao giờ để secret trong code.

4
Push và kiểm tra Actions tab

Push code → vào tab Actions trên GitHub. Xem workflow chạy realtime. Click vào từng step để xem logs chi tiết khi có lỗi.

4. Badge CI/CD cho README

markdown — README.md badges
# MyApp

[![CI](https://github.com/username/myapp/actions/workflows/ci.yml/badge.svg)](https://github.com/username/myapp/actions/workflows/ci.yml)
[![Deploy](https://github.com/username/myapp/actions/workflows/deploy.yml/badge.svg)](https://github.com/username/myapp/actions/workflows/deploy.yml)
[![codecov](https://codecov.io/gh/username/myapp/branch/main/graph/badge.svg)](https://codecov.io/gh/username/myapp)

> Thay `username/myapp` bằng GitHub username và repo name của bạn
🤖
Dùng Copilot để tạo workflow tùy chỉnh

Hỏi Copilot Chat: "Tạo GitHub Actions workflow cho Node.js + Prisma project, deploy lên Railway, chạy tests với PostgreSQL service" — Copilot sẽ generate YAML hoàn chỉnh theo context project của bạn.

🛠 Thực Hành: Setup CI/CD Cho Project
  1. Tạo file .github/workflows/ci.yml — cấu hình lint + test chạy trên mọi push
  2. Push 1 commit có lỗi ESLint cố ý → xem CI fail, đọc error message trong Actions tab
  3. Sửa lỗi → push lại → xem CI pass (màu xanh ✅)
  4. Thêm CI badge vào README
  5. Bonus: Setup deploy.yml với platform bạn chọn (Railway, Render, hoặc VPS)
✅ Dấu Hiệu Code Đạt Chất Lượng Production
  • npm run lint → 0 errors, 0 warnings (hoặc chỉ intentional suppressions)
  • npm test -- --coverage → ≥70% statement coverage cho utils, ≥50% tổng thể
  • Mọi function public có JSDoc/docstring, mọi API endpoint có mô tả rõ ràng
  • Git log có commit messages theo Conventional Commits (feat:, fix:, docs:...)
  • Code review 5 chiều với Copilot: 0 issues Critical, ≤3 issues Major
  • README đầy đủ — người lạ có thể clone repo và chạy app trong 5 phút

🗒 Tóm Tắt Chương 9

  • ESLint + Prettier + Husky = "safety net" — setup 1 lần, chạy mãi mãi
  • Unit test: test 1 function riêng lẻ, fast, no side effects; Integration: nhiều layers, real DB
  • AI review code theo framework 5 chiều: Correctness, Security, Performance, Maintainability, Tests
  • Refactoring không phải viết lại — là làm code sạch hơn mà không thay đổi behavior
  • Code metrics: coverage >70%, complexity <10, bundle <200KB — đo và cải thiện liên tục
  • Conventional Commits + GitHub Flow: workflow chuyên nghiệp cho solo hoặc team
  • GitHub Actions CI: tự động lint + test + build mỗi lần push — phát hiện lỗi trước khi merge
  • GitHub Actions CD: deploy tự động lên Railway/Render/VPS khi merge vào main
  • GitHub Secrets: lưu credentials an toàn — không bao giờ hardcode trong code
🗺 Sơ Đồ — Pipeline Chất Lượng Code
flowchart LR A["Code"] --> B["ESLint + Prettier"] B --> C["Unit Test · Vitest/pytest"] C --> D["Coverage 80%+"] D --> E["GitHub Actions CI/CD"]

🧠 Kiểm Tra Kiến Thức Chương 9

Trả lời 5 câu để củng cố. Đạt ≥ 80% sẽ tự động đánh dấu hoàn thành chương.

1. Công cụ nào kiểm tra và bắt lỗi code JavaScript/TypeScript?

ESLint kiểm tra code, bắt lỗi và áp chuẩn viết code cho JS/TS.

2. TDD nghĩa là gì?

TDD (Test-Driven Development) = viết test trước, rồi viết code cho test pass.

3. Framework test nhanh cho dự án JavaScript/TypeScript?

Vitest là framework test nhanh, tương thích Vite, cho JS/TS.

4. Mục tiêu coverage thường được khuyến nghị?

Mục tiêu coverage thường là 80% trở lên để đảm bảo chất lượng.

5. Đâu là ví dụ đúng theo Conventional Commits?

Conventional Commits dùng tiền tố như feat:, fix: để mô tả rõ thay đổi.
Zalo: 0898 619 966 Z Gọi: 0898 619 966