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.

你将学到:


1. 测试概述

(1) 为什么要写测试

不写测试 写测试
手动验证,每次 30 分钟 自动运行,3 秒完成
改一处,到处出 bug 改一处,测试告诉你哪里断了
上线前焦虑 绿色通过,安心部署
重构不敢动 重构后测试护航

▶ 示例:(2) 测试金字塔

100%
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

(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
💡 本课使用 Jest + Supertest 组合,这是 Node.js 社区最主流的测试方案。



2. Jest 单元测试

▶ 示例:(1) 安装与配置

BASH
npm install --save-dev jest

package.json 中添加 test 脚本:

JSON
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Jest 默认零配置,会自动查找 *.test.js*.spec.js 文件。

▶ 示例:(2) 基本语法:describe / it / expect

JAVASCRIPT
// 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 };
▶ 试一试
JAVASCRIPT
// 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');
  });
});

运行:

BASH
npm test

输出:

TEXT 📖 仅展示
 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) 异步测试

JAVASCRIPT
// 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 };
▶ 试一试
JAVASCRIPT
// 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 请求、文件系统),让测试只关注当前模块逻辑。

JAVASCRIPT
// 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 };
JAVASCRIPT
// 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 基础断言

JAVASCRIPT
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) 安装与原理

BASH
npm install --save-dev supertest

Supertest 在内存中启动 Express 应用,发送 HTTP 请求并断言响应,无需真实监听端口。

100%
flowchart LR
    A["Test File"] -->|"request(app)"| B["Express App<br/>(in-memory)"]
    B -->|"response"| A
    A -->|"assert"| C["Jest expect"]

▶ 示例:(2) 测试 Express API

JAVASCRIPT
// 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 对象。

JAVASCRIPT
// 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

JAVASCRIPT
// 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 健康检查测试

JAVASCRIPT
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 内置调试器,无需安装额外工具。

BASH
node inspect app.js

然后在 Chrome 浏览器打开 chrome://inspect,点击 "Open dedicated DevTools for Node"。

常用调试操作:

操作 DevTools 快捷键 命令行
继续执行 F8 c
单步跳过 F10 n
单步进入 F11 s
单步跳出 Shift+F11 o
设置断点 点击行号 setBreakpoint()

▶ 示例:(2) 在代码中设置断点

JAVASCRIPT
// 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'));
▶ 试一试
BASH
node inspect debug-demo.js

▶ 示例:(3) console 调试技巧

JAVASCRIPT
// 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) 安装与基本配置

BASH
npm install winston
npm install winston-daily-rotate-file
JAVASCRIPT
// 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 应用中使用

JAVASCRIPT
// 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;
▶ 试一试

日志文件输出示例:

JSON
{"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 结构化日志

JAVASCRIPT
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 日志记录 —— 不同日志级别(infowarnerror)和附带上下文元数据。



6. 测试覆盖率与 TDD

(1) 测试覆盖率

Jest 内置 Istanbul 覆盖率工具:

BASH
npm run test:coverage

输出:

TEXT 📖 仅展示
----------|---------|----------|---------|---------|-------------------
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%
💡 追求 100% 覆盖率投入产出比低,80% 覆盖率能捕获大部分 bug。

▶ 示例:(2) TDD 工作流(Red-Green-Refactor)

100%
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
  1. Red:先写测试(会失败,因为功能还没实现)
  2. Green:写最少代码让测试通过
  3. Refactor:重构代码,测试依然通过

▶ 示例:TDD 字符串工具函数

JAVASCRIPT
// 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');
    });
  });
});
▶ 试一试
JAVASCRIPT
// 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 — 应用代码

JAVASCRIPT 📖 仅展示
// 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 };
逻辑代码 44 行(超过 40 行限制,仅展示)

Step 2 — 完整测试套件

JAVASCRIPT
// 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);
  });
});

运行:

BASH
npm test

输出:

TEXT 📖 仅展示
 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

❓ 常见问题

Q Jest 和 Mocha 该选哪个?
A Jest 零配置、内置 mock/coverage,适合大多数项目;Mocha 灵活但需搭配 chai/sinon/istanbul,适合需要深度定制的项目。新项目推荐 Jest。
Q 单元测试和集成测试有什么区别?
A 单元测试隔离测试单个函数/模块,使用 mock 替换外部依赖,速度快;集成测试验证多个模块协作(如 API + 数据库),使用真实组件,速度慢但更接近真实场景。
Q 测试需要连接真实数据库吗?
A 单元测试不应连真实数据库,用 mock 模拟;集成测试可以用内存数据库(如 SQLite :memory:)或测试专用数据库实例;E2E 测试使用测试环境数据库。
Q winston 日志级别有哪些?
A 从低到高为 error → warn → info → http → verbose → debug → silly。设置 level 为 info 时,只输出 info 及以上级别。
Q 如何调试 Jest 测试本身?
A 运行 node --inspect-brk node_modules/.bin/jest --runInBand,然后在 Chrome DevTools 中调试。--runInBand 确保 Jest 在单线程中运行,断点才能生效。
Q Supertest 测试需要启动真实服务器吗?
A 不需要。Supertest 通过 http.createServer(app) 在内存中创建服务器,不占用端口,测试结束自动关闭。

📖 小节


📝 作业

  1. 用 Jest 为以下函数编写单元测试:function isPalindrome(str) { return str === str.split('').reverse().join(''); },覆盖正常回文、非回文、空字符串、大小写混合等情况
  2. 创建一个 Express API(CRUD for /api/books),用 Supertest 编写至少 5 个集成测试(创建、查询、更新、删除、404)
  3. 为一个现有 Express 应用添加 winston 日志中间件,记录每个请求的 method/url/status/duration,并配置日志文件按天轮转
  4. 用 TDD 方式开发一个 formatCurrency(amount, currency) 函数:先写失败测试(如 formatCurrency(1234.5, 'USD')'$1,234.50'),再写实现代码
  5. 运行 jest --coverage,找出项目中覆盖率低于 80% 的文件,补充测试用例使覆盖率达标

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏