Node.js: 任务 API 项目(下)

最后更新:2026-08-26

1. 收尾上线:Alice 的第三天

第三天,Alice 写测试和 API 文档,Bob 配置 Docker 部署。"代码能跑不等于能上线,"Bob 说,"测试保证质量,文档保证可维护,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 流程

100%
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 镜像 → 推送到仓库 → 部署到服务器。

📖 小节

📝 作业

  1. 编写至少 5 个测试用例覆盖认证和任务 CRUD,运行 npm test 确保全部通过。
  2. 为所有路由添加 Swagger 注解,启动后访问 /api-docs 查看文档。
  3. 编写 Dockerfile 和 docker-compose.yml,运行 docker-compose up --build 验证部署。
  4. 添加 /api/health 健康检查端点,在 Docker 中配置 HEALTHCHECK 指令。
  5. 使用自定义 AppError 类替换所有 throw new Error(),确保错误响应格式统一。

Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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