Node.js: 测试与调试
最后更新:2026-08-26
Bob wrote a RESTful API for an e-commerce platform, and every time he added a new feature, something else broke. Manual testing with Postman took 30 minutes per deployment, and he still missed edge cases. He introduced Jest for unit testing and Supertest for API integration testing — now npm test runs 50 tests in 3 seconds, and every deployment is backed by an automated CI check. He also added winston for structured logging, so when a bug slips through, the logs tell him exactly what went wrong.
你将学到:
- Jest 单元测试(describe / it / expect / mock)
- Supertest API 集成测试(HTTP 请求模拟)
- Node.js 内置调试器(node inspect / Chrome DevTools)
- winston 结构化日志(transports / format / rotation)
- 测试覆盖率与 TDD 工作流
1. 测试概述
(1) 为什么要写测试
| 不写测试 | 写测试 |
|---|---|
| 手动验证,每次 30 分钟 | 自动运行,3 秒完成 |
| 改一处,到处出 bug | 改一处,测试告诉你哪里断了 |
| 上线前焦虑 | 绿色通过,安心部署 |
| 重构不敢动 | 重构后测试护航 |
▶ 示例:(2) 测试金字塔
graph TD
A["Unit Tests<br/>Many / Fast / Cheap"] --> B["Integration Tests<br/>Moderate / Slower"]
B --> C["E2E Tests<br/>Few / Slow / Expensive"]
style A fill:#4CAF50,color:#fff
style B fill:#FF9800,color:#fff
style C fill:#F44336,color:#fff
- Unit Tests:测试单个函数/模块,速度快,数量最多
- Integration Tests:测试模块间协作(如 API + 数据库),速度中等
- E2E Tests:模拟用户操作,速度最慢,数量最少
(3) Node.js 测试工具对比
| 工具 | 类型 | 特点 | 适合场景 |
|---|---|---|---|
| Jest | 单元 + 集成 | 零配置、内置 mock/coverage、快照测试 | 通用首选 |
| Mocha | 单元 + 集成 | 灵活、插件丰富、需搭配 chai/sinon | 定制化需求 |
| Vitest | 单元 | Vite 生态、ESM 原生、极快 | Vite 项目 |
| node:test | 单元 | Node.js 内置、零依赖 | 简单项目 |
| Supertest | HTTP 测试 | 模拟 HTTP 请求、测 Express 路由 | API 集成测试 |
| Playwright | E2E | 浏览器自动化、多浏览器支持 | 前端 E2E |
2. Jest 单元测试
▶ 示例:(1) 安装与配置
npm install --save-dev jest
在 package.json 中添加 test 脚本:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Jest 默认零配置,会自动查找 *.test.js 或 *.spec.js 文件。
▶ 示例:(2) 基本语法:describe / it / expect
// math.js — the module to test
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
module.exports = { add, subtract, multiply, divide };
// math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math utilities', () => {
test('add should sum two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
test('subtract should return the difference', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiply should return the product', () => {
expect(multiply(3, 4)).toBe(12);
});
test('divide should throw on zero divisor', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
运行:
npm test
输出:
PASS ./math.test.js
Math utilities
✓ add should sum two numbers (2 ms)
✓ subtract should return the difference
✓ multiply should return the product
✓ divide should throw on zero divisor (1 ms)
Tests: 4 passed, 4 total
Time: 0.5s
(3) 常用匹配器(Matchers)
| 匹配器 | 含义 | 示例 |
|---|---|---|
toBe |
严格相等(===) | expect(1 + 1).toBe(2) |
toEqual |
深度相等 | expect(obj).toEqual({ a: 1 }) |
toBeTruthy / toBeFalsy |
布尔判断 | expect(flag).toBeTruthy() |
toBeNull / toBeUndefined |
null / undefined | expect(val).toBeNull() |
toThrow |
抛出异常 | expect(fn).toThrow() |
toContain |
数组包含 | expect([1, 2, 3]).toContain(2) |
toMatch |
正则匹配 | expect('hello').toMatch(/ell/) |
toHaveLength |
长度检查 | expect('abc').toHaveLength(3) |
resolves / rejects |
Promise 结果 | expect(promise).resolves.toBe(5) |
toHaveBeenCalled |
mock 被调用 | expect(fn).toHaveBeenCalled() |
▶ 示例:(4) 异步测试
// async-functions.js
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User_${id}` });
}, 100);
});
}
async function getUserEmail(id) {
const user = await fetchUser(id);
return `${user.name.toLowerCase()}@example.com`;
}
module.exports = { fetchUser, getUserEmail };
// async-functions.test.js
const { fetchUser, getUserEmail } = require('./async-functions');
describe('Async function tests', () => {
test('fetchUser returns user object', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'User_1' });
});
test('getUserEmail returns formatted email', async () => {
const email = await getUserEmail(42);
expect(email).toBe('user_42@example.com');
});
test('fetchUser resolves within 200ms', async () => {
await expect(fetchUser(1)).resolves.toHaveProperty('id', 1);
});
});
(5) Mock 模拟
Mock 用于隔离外部依赖(数据库、HTTP 请求、文件系统),让测试只关注当前模块逻辑。
// email-service.js
class EmailService {
send(to, subject, body) {
// actual SMTP call — too slow for unit tests
console.log(`Sending email to ${to}: ${subject}`);
return true;
}
}
class UserService {
constructor(emailService) {
this.emailService = emailService;
}
register(name, email) {
// business logic: validate + send welcome email
if (!email.includes('@')) throw new Error('Invalid email');
this.emailService.send(email, 'Welcome', `Hello ${name}!`);
return { name, email, status: 'registered' };
}
}
module.exports = { EmailService, UserService };
// email-service.test.js
const { EmailService, UserService } = require('./email-service');
describe('UserService', () => {
let mockEmailService;
let userService;
beforeEach(() => {
mockEmailService = {
send: jest.fn().mockReturnValue(true),
};
userService = new UserService(mockEmailService);
});
test('register should call emailService.send', () => {
const result = userService.register('Alice', 'alice@example.com');
expect(result).toEqual({
name: 'Alice',
email: 'alice@example.com',
status: 'registered',
});
expect(mockEmailService.send).toHaveBeenCalledWith(
'alice@example.com',
'Welcome',
'Hello Alice!'
);
});
test('register should throw on invalid email', () => {
expect(() => userService.register('Bob', 'invalid')).toThrow('Invalid email');
expect(mockEmailService.send).not.toHaveBeenCalled();
});
test('register should send exactly one email', () => {
userService.register('Charlie', 'charlie@example.com');
expect(mockEmailService.send).toHaveBeenCalledTimes(1);
});
});
▶ 示例:Jest 基础断言
test('basic matchers', () => {
expect(2 + 2).toBe(4);
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
expect([1, 2, 3]).toContain(2);
expect('hello world').toMatch(/world/);
expect(() => { throw new Error('fail'); }).toThrow('fail');
});
展示最常用的 Jest 匹配器:toBe(原始值)、toEqual(对象)、toContain(数组)、toMatch(字符串)、toThrow(错误测试)。
3. Supertest API 集成测试
▶ 示例:(1) 安装与原理
npm install --save-dev supertest
Supertest 在内存中启动 Express 应用,发送 HTTP 请求并断言响应,无需真实监听端口。
flowchart LR
A["Test File"] -->|"request(app)"| B["Express App<br/>(in-memory)"]
B -->|"response"| A
A -->|"assert"| C["Jest expect"]
▶ 示例:(2) 测试 Express API
// app.js — the Express app (export without listen)
const express = require('express');
const app = express();
app.use(express.json());
let products = [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Phone', price: 499.99 },
];
app.get('/api/products', (req, res) => {
res.json(products);
});
app.get('/api/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ error: 'Product not found' });
res.json(product);
});
app.post('/api/products', (req, res) => {
const { name, price } = req.body;
if (!name || price == null) {
return res.status(400).json({ error: 'Name and price are required' });
}
const newProduct = { id: products.length + 1, name, price };
products.push(newProduct);
res.status(201).json(newProduct);
});
module.exports = app;
app.js 只导出 app,不在模块内调用 app.listen()。listen 放在 server.js 中,这样 Supertest 可以直接测试 app 对象。
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('Products API', () => {
test('GET /api/products should return product list', async () => {
const response = await request(app).get('/api/products');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name', 'Laptop');
});
test('GET /api/products/:id should return a single product', async () => {
const response = await request(app).get('/api/products/1');
expect(response.status).toBe(200);
expect(response.body.id).toBe(1);
});
test('GET /api/products/:id should return 404 for missing product', async () => {
const response = await request(app).get('/api/products/999');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('error', 'Product not found');
});
test('POST /api/products should create a new product', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Tablet', price: 299.99 });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('name', 'Tablet');
expect(response.body).toHaveProperty('id', 3);
});
test('POST /api/products should return 400 for missing fields', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Incomplete Product' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
▶ 示例:(3) 测试带认证的 API
// protected-routes.test.js
const request = require('supertest');
const app = require('./app');
describe('Protected API routes', () => {
let token;
beforeAll(async () => {
// login to get JWT token
const response = await request(app)
.post('/api/auth/login')
.send({ username: 'alice', password: 'secret123' });
token = response.body.token;
});
test('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'alice');
});
test('should reject access without token', async () => {
const response = await request(app).get('/api/profile');
expect(response.status).toBe(401);
});
test('should reject access with invalid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid_token_here');
expect(response.status).toBe(401);
});
});
▶ 示例:API 健康检查测试
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/health', (req, res) => res.json({ status: 'ok' }));
describe('GET /health', () => {
it('returns 200 with status ok', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});
});
一个极简的 API 健康检查测试 — 创建 Express 应用、发送 GET 请求、断言 JSON 响应体和状态码。
4. Node.js 调试
(1) node inspect + Chrome DevTools
Node.js 内置调试器,无需安装额外工具。
node inspect app.js
然后在 Chrome 浏览器打开 chrome://inspect,点击 "Open dedicated DevTools for Node"。
常用调试操作:
| 操作 | DevTools 快捷键 | 命令行 |
|---|---|---|
| 继续执行 | F8 | c |
| 单步跳过 | F10 | n |
| 单步进入 | F11 | s |
| 单步跳出 | Shift+F11 | o |
| 设置断点 | 点击行号 | setBreakpoint() |
▶ 示例:(2) 在代码中设置断点
// debug-demo.js
function calculateDiscount(price, memberLevel) {
debugger; // execution pauses here in inspector
let discount = 0;
if (memberLevel === 'gold') discount = 0.2;
else if (memberLevel === 'silver') discount = 0.1;
const finalPrice = price * (1 - discount);
return finalPrice;
}
console.log(calculateDiscount(100, 'gold'));
console.log(calculateDiscount(100, 'silver'));
console.log(calculateDiscount(100, 'unknown'));
node inspect debug-demo.js
▶ 示例:(3) console 调试技巧
// console debugging methods
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'admin' },
];
// Table display
console.table(users);
// Measure execution time
console.time('database-query');
// ... query logic ...
console.timeEnd('database-query');
// Stack trace
console.trace('Where is this called from?');
// Grouped output
console.group('User Details');
console.log('Name: Alice');
console.log('Role: admin');
console.groupEnd();
5. winston 结构化日志
(1) 为什么用 winston 而非 console.log
| console.log | winston |
|---|---|
| 无日志级别 | 支持 debug/info/warn/error |
| 无文件输出 | 支持 Console + File + HTTP 等多种 transport |
| 无格式化 | 支持 JSON / timestamp / 自定义格式 |
| 无日志轮转 | 搭配 winston-daily-rotate-file 自动轮转 |
| 生产环境不推荐 | 生产环境首选 |
▶ 示例:(2) 安装与基本配置
npm install winston
npm install winston-daily-rotate-file
// logger.js
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'my-app' },
transports: [
// Error logs — separate file
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '14d',
}),
// All logs — combined file
new DailyRotateFile({
filename: 'logs/combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
// Development: also log to console with colorized output
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
module.exports = logger;
▶ 示例:(3) 在 Express 应用中使用
// app-with-logger.js
const express = require('express');
const logger = require('./logger');
const app = express();
app.use(express.json());
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
});
app.get('/api/health', (req, res) => {
logger.debug('Health check requested');
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
logger.info('User lookup', { userId });
if (isNaN(userId)) {
logger.warn('Invalid user ID format', { userId });
return res.status(400).json({ error: 'Invalid user ID' });
}
// Simulate user lookup
const user = { id: parseInt(userId), name: 'Alice' };
logger.info('User found', { userId: user.id, name: user.name });
res.json(user);
});
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
日志文件输出示例:
{"level":"info","message":"HTTP request","service":"my-app","timestamp":"2026-07-13 10:30:00","method":"GET","url":"/api/users/1","status":200,"duration":"15ms"}
{"level":"warn","message":"Invalid user ID format","service":"my-app","timestamp":"2026-07-13 10:30:05","userId":"abc"}
{"level":"error","message":"Unhandled error","service":"my-app","timestamp":"2026-07-13 10:31:00","error":"Cannot read property 'name' of undefined","stack":"TypeError: Cannot read property..."}
▶ 示例:winston 结构化日志
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
logger.info('服务器启动', { port: 3000, env: 'development' });
logger.warn('内存使用过高', { used: '850MB' });
logger.error('数据库连接失败', { db: 'main', retry: 3 });
演示使用 winston 进行结构化 JSON 日志记录 —— 不同日志级别(info、warn、error)和附带上下文元数据。
6. 测试覆盖率与 TDD
(1) 测试覆盖率
Jest 内置 Istanbul 覆盖率工具:
npm run test:coverage
输出:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 90.5 | 85.71 | 100 | 90.5 |
math.js | 100 | 100 | 100 | 100 |
app.js | 88.24 | 83.33 | 100 | 88.24 | 45-48
----------|---------|----------|---------|---------|-------------------
覆盖率维度说明:
| 维度 | 含义 | 建议目标 |
|---|---|---|
| Statements | 执行的语句比例 | ≥ 80% |
| Branches | 执行的条件分支比例 | ≥ 75% |
| Functions | 调用的函数比例 | ≥ 85% |
| Lines | 执行的代码行比例 | ≥ 80% |
▶ 示例:(2) TDD 工作流(Red-Green-Refactor)
flowchart LR
A["🔴 Red<br/>Write failing test"] --> B["🟢 Green<br/>Write minimal code to pass"]
B --> C["🔵 Refactor<br/>Improve code while tests pass"]
C --> A
- Red:先写测试(会失败,因为功能还没实现)
- Green:写最少代码让测试通过
- Refactor:重构代码,测试依然通过
▶ 示例:TDD 字符串工具函数
// string-utils.test.js — write test FIRST (Red)
const { capitalize, truncate, slugify } = require('./string-utils');
describe('StringUtils', () => {
describe('capitalize', () => {
test('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('should handle empty string', () => {
expect(capitalize('')).toBe('');
});
test('should handle already capitalized', () => {
expect(capitalize('Hello')).toBe('Hello');
});
});
describe('truncate', () => {
test('should truncate long strings', () => {
expect(truncate('Hello World', 5)).toBe('Hello...');
});
test('should not truncate short strings', () => {
expect(truncate('Hi', 10)).toBe('Hi');
});
});
describe('slugify', () => {
test('should convert to URL slug', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('should handle special characters', () => {
expect(slugify('C# & .NET!')).toBe('c-and-net');
});
});
});
// string-utils.js — write implementation AFTER test (Green)
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
function truncate(str, maxLength) {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + '...';
}
function slugify(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { capitalize, truncate, slugify };
7. 综合示例:为任务管理 API 编写完整测试套件
▶ 示例:Tasks API Test Suite
Step 1 — 应用代码
// tasks-app.js
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [];
let nextId = 1;
function resetTasks() {
tasks = [];
nextId = 1;
}
app.get('/api/tasks', (req, res) => {
const { status, priority } = req.query;
let filtered = tasks;
if (status) filtered = filtered.filter(t => t.status === status);
if (priority) filtered = filtered.filter(t => t.priority === priority);
res.json(filtered);
});
app.get('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.post('/api/tasks', (req, res) => {
const { title, priority = 'medium' } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const task = { id: nextId++, title, priority, status: 'pending' };
tasks.push(task);
res.status(201).json(task);
});
app.patch('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
const { title, status, priority } = req.body;
if (title) task.title = title;
if (status) task.status = status;
if (priority) task.priority = priority;
res.json(task);
});
app.delete('/api/tasks/:id', (req, res) => {
const index = tasks.findIndex(t => t.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Task not found' });
const deleted = tasks.splice(index, 1);
res.json(deleted[0]);
});
module.exports = { app, resetTasks };
Step 2 — 完整测试套件
// tasks-app.test.js
const request = require('supertest');
const { app, resetTasks } = require('./tasks-app');
beforeEach(() => {
resetTasks();
});
describe('Tasks API — CRUD operations', () => {
test('should create a task', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: 'Write tests', priority: 'high' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: 1,
title: 'Write tests',
priority: 'high',
status: 'pending',
});
});
test('should reject task without title', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ priority: 'high' });
expect(response.status).toBe(400);
expect(response.body.error).toBe('Title is required');
});
test('should list all tasks', async () => {
await request(app).post('/api/tasks').send({ title: 'Task 1' });
await request(app).post('/api/tasks').send({ title: 'Task 2' });
const response = await request(app).get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
});
test('should filter tasks by status', async () => {
await request(app).post('/api/tasks').send({ title: 'Pending task' });
const createRes = await request(app).post('/api/tasks').send({ title: 'Done task' });
await request(app).patch(`/api/tasks/${createRes.body.id}`).send({ status: 'done' });
const response = await request(app).get('/api/tasks?status=done');
expect(response.body).toHaveLength(1);
expect(response.body[0].status).toBe('done');
});
test('should update a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'Old title' });
const response = await request(app)
.patch(`/api/tasks/${createRes.body.id}`)
.send({ title: 'New title', status: 'done' });
expect(response.status).toBe(200);
expect(response.body.title).toBe('New title');
expect(response.body.status).toBe('done');
});
test('should delete a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'To delete' });
const response = await request(app).delete(`/api/tasks/${createRes.body.id}`);
expect(response.status).toBe(200);
const listRes = await request(app).get('/api/tasks');
expect(listRes.body).toHaveLength(0);
});
test('should return 404 for non-existent task', async () => {
const response = await request(app).get('/api/tasks/999');
expect(response.status).toBe(404);
});
});
运行:
npm test
输出:
PASS ./tasks-app.test.js
Tasks API — CRUD operations
✓ should create a task (15 ms)
✓ should reject task without title (3 ms)
✓ should list all tasks (4 ms)
✓ should filter tasks by status (8 ms)
✓ should update a task (5 ms)
✓ should delete a task (5 ms)
✓ should return 404 for non-existent task (2 ms)
Tests: 7 passed, 7 total
Time: 0.8s
❓ 常见问题
node --inspect-brk node_modules/.bin/jest --runInBand,然后在 Chrome DevTools 中调试。--runInBand 确保 Jest 在单线程中运行,断点才能生效。http.createServer(app) 在内存中创建服务器,不占用端口,测试结束自动关闭。📖 小节
- 测试金字塔:单元测试(多快便宜)→ 集成测试(中等)→ E2E 测试(少慢贵)
- Jest 零配置单元测试:describe/it/expect + mock 模拟外部依赖
- Supertest 在内存中测试 Express API:request(app).get/post/patch/delete
- Node.js 内置 node inspect 调试器,搭配 Chrome DevTools 可视化断点
- winston 提供结构化日志:多级别 + 多 transport + 日志轮转
- 测试覆盖率 80% 以上即可,TDD 工作流 Red→Green→Refactor 提高代码质量
📝 作业
- 用 Jest 为以下函数编写单元测试:
function isPalindrome(str) { return str === str.split('').reverse().join(''); },覆盖正常回文、非回文、空字符串、大小写混合等情况 - 创建一个 Express API(CRUD for
/api/books),用 Supertest 编写至少 5 个集成测试(创建、查询、更新、删除、404) - 为一个现有 Express 应用添加 winston 日志中间件,记录每个请求的 method/url/status/duration,并配置日志文件按天轮转
- 用 TDD 方式开发一个
formatCurrency(amount, currency)函数:先写失败测试(如formatCurrency(1234.5, 'USD')→'$1,234.50'),再写实现代码 - 运行
jest --coverage,找出项目中覆盖率低于 80% 的文件,补充测试用例使覆盖率达标