Node.js: 任务 API 项目(下)
最后更新:2026-08-26
1. 收尾上线:Alice 的第三天
第三天,Alice 写测试和 API 文档,Bob 配置 Docker 部署。"代码能跑不等于能上线,"Bob 说,"测试保证质量,文档保证可维护,Docker 保证一致性。"
- Jest + Supertest 是 Node.js API 测试的标准组合
- 统一错误处理让 API 响应格式一致
- Swagger 自动生成文档,代码即文档
- Docker 容器化确保开发与生产环境一致
- 健康检查是生产部署的必备配置
2. Jest + Supertest 测试
(1) 测试文件结构
| 文件 | 说明 |
|---|---|
tests/setup.js |
测试环境配置(连接测试数据库) |
tests/auth.test.js |
认证模块测试 |
tests/tasks.test.js |
任务模块测试 |
tests/helpers.js |
测试辅助函数(创建测试用户等) |
▶ 示例:测试环境配置
JAVASCRIPT
// tests/setup.js
process.env.JWT_SECRET = 'test-secret';
process.env.MONGO_URI = 'mongodb://localhost:27017/task-manager-test';
const mongoose = require('mongoose');
beforeAll(async () => await mongoose.connect(process.env.MONGO_URI));
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
▶ 示例:认证模块测试
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
describe('Auth API', () => {
beforeEach(async () => await User.deleteMany({}));
test('should register a new user', async () => {
const res = await request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(201);
expect(res.body.token).toBeDefined();
expect(res.body.user.username).toBe('alice');
});
test('should login existing user', async () => {
await request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
const res = await request(app).post('/api/auth/login').send({
email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
});
test('should reject invalid credentials', async () => {
const res = await request(app).post('/api/auth/login').send({
email: 'noone@test.com', password: 'wrong'
});
expect(res.status).toBe(401);
});
});
▶ 示例:任务模块测试
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register').send({
username: 'bob', email: 'bob@test.com', password: '123456'
});
token = reg.body.token;
userId = reg.body.user.id;
});
test('should create a task', async () => {
const res = await request(app).post('/api/tasks').set('Authorization', `Bearer ${token}`).send({ title: 'Write tests' });
expect(res.status).toBe(201);
expect(res.body.title).toBe('Write tests');
});
test('should get tasks list', async () => {
await Task.create({ title: 'Task1', assignedTo: userId });
const res = await request(app).get('/api/tasks').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(1);
});
test('should deny access without token', async () => {
const res = await request(app).get('/api/tasks');
expect(res.status).toBe(401);
});
3. 错误处理统一封装
▶ 示例:自定义错误类
JAVASCRIPT
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
module.exports = AppError;
▶ 示例:全局错误处理中间件
JAVASCRIPT
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
res.status(statusCode).json({
status: statusCode >= 400 && statusCode < 500 ? 'fail' : 'error',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
4. Swagger API 文档
(1) Swagger 注解常用标签
| 标签 | 说明 | 使用位置 |
|---|---|---|
@openapi |
路径与操作定义 | 路由文件 |
@swagger |
组件定义(Schema) | 文档配置 |
@tags |
API 分组 | 路由文件 |
@security |
认证方式引用 | 需认证端点 |
@produces |
响应格式 | 路由文件 |
@parameters |
请求参数 | 路由文件 |
▶ 示例:Swagger 配置
JAVASCRIPT
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0', description: '团队任务管理 RESTful API' },
servers: [{ url: 'http://localhost:3000/api' }],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
}
}
},
apis: ['./src/routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = { swaggerUi, specs };
▶ 示例:路由中的 Swagger 注解
JAVASCRIPT
/**
* @openapi
* /auth/register:
* post:
* tags: [Auth]
* summary: 用户注册
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [username, email, password]
* properties:
* username: { type: string }
* email: { type: string, format: email }
* password: { type: string, minLength: 6 }
* responses:
* 201:
* description: 注册成功
* 400:
* description: 参数错误
*/
router.post('/register', async (req, res, next) => { /* ... */ });
5. Docker 部署
(1) Docker 文件说明
| 文件 | 说明 |
|---|---|
Dockerfile |
构建 Node.js 应用镜像 |
docker-compose.yml |
编排应用 + MongoDB 容器 |
.dockerignore |
排除 node_modules 等不必要文件 |
(2) 项目完成度检查清单
| 检查项 | 状态 |
|---|---|
| 项目可正常启动 | ☐ |
| 注册/登录 API 可用 | ☐ |
| Task CRUD 可用 | ☐ |
| 分页筛选可用 | ☐ |
| 权限控制正常 | ☐ |
| 测试通过 | ☐ |
| Swagger 文档可访问 | ☐ |
| Docker 构建成功 | ☐ |
| 健康检查端点正常 | ☐ |
| 错误处理统一 | ☐ |
▶ 示例:Dockerfile
DOCKERFILE
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
▶ 示例:docker-compose.yml
YAML
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
ports:
- "27017:27017"
volumes:
mongo-data:
▶ 示例:健康检查端点
JAVASCRIPT
router.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
});
6. CI/CD 流程
graph LR
A[代码推送] --> B[运行 Jest 测试]
B -->|通过| C[构建 Docker 镜像]
B -->|失败| D[通知开发者]
C --> E[推送镜像到 Registry]
E --> F[部署到服务器]
F --> G[健康检查]
G -->|通过| H[上线完成]
G -->|失败| I[回滚版本]
▶ 示例:项目启动命令汇总
BASH
# 开发环境
npm run dev
# 运行测试
npm test
# Docker 构建
docker-compose up --build
# 生产部署
docker-compose -f docker-compose.prod.yml up -d
7. 综合示例:测试+文档+Docker 完整配置
JAVASCRIPT
// tests/tasks.test.js
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register')
.send({ username: 'testuser', email: 'test@test.com', password: '123456' });
token = reg.body.token;
userId = reg.body.user.id;
});
describe('Task API', () => {
test('POST /api/tasks - create task', async () => {
const res = await request(app).post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({ title: 'My Task', priority: 'high' });
expect(res.status).toBe(201);
});
test('GET /api/tasks - list with pagination', async () => {
for (let i = 0; i < 15; i++) {
await Task.create({ title: `Task ${i}`, assignedTo: userId });
}
const res = await request(app).get('/api/tasks?page=2&limit=5')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(5);
expect(res.body.page).toBe(2);
});
test('DELETE /api/tasks/:id - owner can delete', async () => {
const task = await Task.create({ title: 'To Delete', assignedTo: userId });
const res = await request(app).delete(`/api/tasks/${task._id}`)
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
});
});
JAVASCRIPT
// src/config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const specs = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0' },
servers: [{ url: '/api' }],
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } } }
},
apis: ['./src/routes/*.js']
});
module.exports = { swaggerUi, specs };
DOCKERFILE
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
YAML
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports: ["3000:3000"]
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=change_me_in_prod
depends_on: [mongo]
mongo:
image: mongo:7
volumes: [mongo-data:/data/db]
volumes:
mongo-data:
❓ 常见问题
Q Jest 和 Mocha 怎么选?
A Jest 开箱即用、零配置、内置断言和覆盖率;Mocha 更灵活但需搭配 chai/sinon。Node.js API 测试推荐 Jest + Supertest。
Q Supertest 如何测试需要认证的接口?
A 先调用登录接口获取 token,然后在后续请求中用 .set('Authorization', 'Bearer ' + token) 传递。
Q Swagger 文档要手动写吗?
A 可以用 swagger-jsdoc 从 JSDoc 注释自动生成,或用 swagger-ui-express 展示。注释即文档,维护成本低。
Q Docker 镜像体积如何优化?
A 使用 alpine 基础镜像、多阶段构建(构建阶段安装依赖,运行阶段只拷贝产物)、.dockerignore 排除不必要文件。
Q CI/CD 流程怎么搭建?
A GitHub 项目用 GitHub Actions:push 触发 → 安装依赖 → 运行测试 → 构建 Docker 镜像 → 推送到仓库 → 部署到服务器。
- Q: API 文档为什么要自动生成? A: 手动维护文档容易与代码脱节,Swagger 注解写在代码中,改代码即改文档,保证一致性。
- Q: 测试要覆盖多少? A: 核心业务逻辑建议 80% 以上,至少覆盖注册、登录、CRUD、权限控制等关键路径,边界情况也要测到。
- Q: Docker 部署比裸机好在哪? A: 环境一致性(消除"在我机器上能跑"问题)、快速部署、资源隔离、易于 CI/CD 集成和水平扩展。
- Q: 生产环境要注意什么? A: JWT_SECRET 使用强密钥、MongoDB 开启认证、启用 HTTPS、限制 CORS 来源、设置 rate limiting、日志收集。
- Q: 如何做健康检查? A: 提供
/api/health端点返回应用+数据库状态,Docker HEALTHCHECK 或 Kubernetes livenessProbe 定期调用。 - Q: 测试数据库和生产数据库会冲突吗? A: 测试使用独立数据库(如
task-manager-test),每个测试套件前后清理数据,不会影响生产。
📖 小节
- 收尾上线:Alice 的第三天的核心概念与使用方法
- Jest + Supertest 测试的核心概念与使用方法
- 错误处理统一封装的核心概念与使用方法
- Swagger API 文档的核心概念与使用方法
- Docker 部署的核心概念与使用方法
- CI/CD 流程的核心概念与使用方法
- 综合示例:测试+文档+Docker 完整配置的核心概念与使用方法
📝 作业
- 编写至少 5 个测试用例覆盖认证和任务 CRUD,运行
npm test确保全部通过。 - 为所有路由添加 Swagger 注解,启动后访问
/api-docs查看文档。 - 编写 Dockerfile 和 docker-compose.yml,运行
docker-compose up --build验证部署。 - 添加
/api/health健康检查端点,在 Docker 中配置 HEALTHCHECK 指令。 - 使用自定义
AppError类替换所有throw new Error(),确保错误响应格式统一。