Jest Node.js 测试 #

Node.js 测试概述 #

Node.js 后端测试是确保 API 可靠性的重要手段,包括单元测试、集成测试和 API 测试。

text
┌─────────────────────────────────────────────────────────────┐
│                    Node.js 测试内容                          │
├─────────────────────────────────────────────────────────────┤
│  1. 单元测试 - 测试独立函数和模块                            │
│  2. API 测试 - 测试 API 端点                                │
│  3. 数据库测试 - 测试数据库操作                              │
│  4. 中间件测试 - 测试 Express/Koa 中间件                    │
│  5. 集成测试 - 测试多个模块协作                              │
│  6. Mock 外部服务 - 模拟第三方服务                          │
└─────────────────────────────────────────────────────────────┘

环境配置 #

安装依赖 #

bash
npm install --save-dev jest supertest

配置 Jest #

javascript
// jest.config.js
module.exports = {
  testEnvironment: 'node',
  testMatch: ['**/*.test.js'],
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js',
  ],
};

Express 测试 #

基本应用测试 #

javascript
// app.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

app.post('/api/users', (req, res) => {
  const user = { id: Date.now(), ...req.body };
  res.status(201).json(user);
});

module.exports = app;
javascript
// app.test.js
const request = require('supertest');
const app = require('./app');

describe('User API', () => {
  test('GET /api/users', async () => {
    const response = await request(app).get('/api/users');
    
    expect(response.status).toBe(200);
    expect(response.body).toHaveLength(1);
    expect(response.body[0]).toHaveProperty('name', 'John');
  });
  
  test('POST /api/users', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Jane' });
    
    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('name', 'Jane');
    expect(response.body).toHaveProperty('id');
  });
});

带数据库的应用 #

javascript
// app.js
const express = require('express');
const { User } = require('./models');

const app = express();
app.use(express.json());

app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

app.post('/api/users', async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json(user);
});

module.exports = app;
javascript
// app.test.js
const request = require('supertest');
const app = require('./app');
const { User } = require('./models');

jest.mock('./models');

describe('User API', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
  
  test('GET /api/users/:id - success', async () => {
    User.findById.mockResolvedValue({ id: 1, name: 'John' });
    
    const response = await request(app).get('/api/users/1');
    
    expect(response.status).toBe(200);
    expect(response.body).toEqual({ id: 1, name: 'John' });
  });
  
  test('GET /api/users/:id - not found', async () => {
    User.findById.mockResolvedValue(null);
    
    const response = await request(app).get('/api/users/999');
    
    expect(response.status).toBe(404);
    expect(response.body).toEqual({ error: 'User not found' });
  });
  
  test('POST /api/users', async () => {
    User.create.mockResolvedValue({ id: 1, name: 'Jane' });
    
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Jane' });
    
    expect(response.status).toBe(201);
    expect(response.body).toEqual({ id: 1, name: 'Jane' });
  });
});

中间件测试 #

javascript
// authMiddleware.js
function authMiddleware(req, res, next) {
  const token = req.headers.authorization;
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const decoded = verifyToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

module.exports = authMiddleware;
javascript
// authMiddleware.test.js
const authMiddleware = require('./authMiddleware');
const { verifyToken } = require('./jwt');

jest.mock('./jwt');

describe('authMiddleware', () => {
  let req, res, next;
  
  beforeEach(() => {
    req = { headers: {} };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    };
    next = jest.fn();
  });
  
  test('passes with valid token', () => {
    req.headers.authorization = 'valid-token';
    verifyToken.mockReturnValue({ id: 1 });
    
    authMiddleware(req, res, next);
    
    expect(next).toHaveBeenCalled();
    expect(req.user).toEqual({ id: 1 });
  });
  
  test('fails without token', () => {
    authMiddleware(req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({ error: 'No token provided' });
    expect(next).not.toHaveBeenCalled();
  });
  
  test('fails with invalid token', () => {
    req.headers.authorization = 'invalid-token';
    verifyToken.mockImplementation(() => {
      throw new Error('Invalid token');
    });
    
    authMiddleware(req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token' });
  });
});

数据库测试 #

使用测试数据库 #

javascript
// database.test.js
const mongoose = require('mongoose');
const User = require('./models/User');

describe('Database Tests', () => {
  beforeAll(async () => {
    await mongoose.connect('mongodb://localhost:27017/test_db');
  });
  
  afterAll(async () => {
    await mongoose.connection.dropDatabase();
    await mongoose.connection.close();
  });
  
  beforeEach(async () => {
    await User.deleteMany({});
  });
  
  test('creates user', async () => {
    const user = await User.create({
      name: 'John',
      email: 'john@example.com',
    });
    
    expect(user.id).toBeDefined();
    expect(user.name).toBe('John');
  });
  
  test('finds user', async () => {
    await User.create({ name: 'John', email: 'john@example.com' });
    
    const user = await User.findOne({ name: 'John' });
    
    expect(user).toBeDefined();
    expect(user.email).toBe('john@example.com');
  });
  
  test('updates user', async () => {
    const user = await User.create({ name: 'John', email: 'john@example.com' });
    
    user.name = 'Jane';
    await user.save();
    
    const updated = await User.findById(user.id);
    expect(updated.name).toBe('Jane');
  });
  
  test('deletes user', async () => {
    const user = await User.create({ name: 'John', email: 'john@example.com' });
    
    await User.findByIdAndDelete(user.id);
    
    const deleted = await User.findById(user.id);
    expect(deleted).toBeNull();
  });
});

使用内存数据库 #

javascript
// mongodb-memory-server.test.js
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');

let mongoServer;

beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  await mongoose.connect(mongoServer.getUri());
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongoServer.stop();
});

服务层测试 #

javascript
// userService.js
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  
  async getUser(id) {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }
  
  async createUser(data) {
    if (!data.email) {
      throw new Error('Email is required');
    }
    return this.userRepository.create(data);
  }
}

module.exports = UserService;
javascript
// userService.test.js
const UserService = require('./userService');

describe('UserService', () => {
  let userService;
  let mockRepository;
  
  beforeEach(() => {
    mockRepository = {
      findById: jest.fn(),
      create: jest.fn(),
    };
    userService = new UserService(mockRepository);
  });
  
  test('getUser - success', async () => {
    mockRepository.findById.mockResolvedValue({ id: 1, name: 'John' });
    
    const user = await userService.getUser(1);
    
    expect(user).toEqual({ id: 1, name: 'John' });
  });
  
  test('getUser - not found', async () => {
    mockRepository.findById.mockResolvedValue(null);
    
    await expect(userService.getUser(999)).rejects.toThrow('User not found');
  });
  
  test('createUser - success', async () => {
    mockRepository.create.mockResolvedValue({ id: 1, name: 'John', email: 'john@example.com' });
    
    const user = await userService.createUser({
      name: 'John',
      email: 'john@example.com',
    });
    
    expect(user).toHaveProperty('id');
  });
  
  test('createUser - missing email', async () => {
    await expect(userService.createUser({ name: 'John' })).rejects.toThrow('Email is required');
  });
});

外部服务 Mock #

Mock HTTP 请求 #

javascript
// externalApi.js
const axios = require('axios');

async function fetchExternalUser(id) {
  const response = await axios.get(`https://api.example.com/users/${id}`);
  return response.data;
}

module.exports = { fetchExternalUser };
javascript
// externalApi.test.js
const axios = require('axios');
const { fetchExternalUser } = require('./externalApi');

jest.mock('axios');

test('fetchExternalUser', async () => {
  axios.get.mockResolvedValue({
    data: { id: 1, name: 'John' },
  });
  
  const user = await fetchExternalUser(1);
  
  expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
  expect(user).toEqual({ id: 1, name: 'John' });
});

Mock 文件系统 #

javascript
// fileService.js
const fs = require('fs').promises;

async function readConfig(path) {
  const content = await fs.readFile(path, 'utf-8');
  return JSON.parse(content);
}

module.exports = { readConfig };
javascript
// fileService.test.js
const fs = require('fs').promises;
const { readConfig } = require('./fileService');

jest.mock('fs', () => ({
  promises: {
    readFile: jest.fn(),
  },
}));

test('readConfig', async () => {
  fs.readFile.mockResolvedValue('{"key": "value"}');
  
  const config = await readConfig('/path/to/config.json');
  
  expect(config).toEqual({ key: 'value' });
});

错误处理测试 #

javascript
// errorHandler.js
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  
  if (err.name === 'ValidationError') {
    return res.status(400).json({ error: err.message });
  }
  
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  res.status(500).json({ error: 'Internal Server Error' });
}

module.exports = errorHandler;
javascript
// errorHandler.test.js
const errorHandler = require('./errorHandler');

describe('errorHandler', () => {
  let req, res, next;
  
  beforeEach(() => {
    req = {};
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    };
    next = jest.fn();
  });
  
  test('handles ValidationError', () => {
    const err = new Error('Invalid data');
    err.name = 'ValidationError';
    
    errorHandler(err, req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith({ error: 'Invalid data' });
  });
  
  test('handles UnauthorizedError', () => {
    const err = new Error('Unauthorized');
    err.name = 'UnauthorizedError';
    
    errorHandler(err, req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
  });
  
  test('handles generic error', () => {
    const err = new Error('Something went wrong');
    
    errorHandler(err, req, res, next);
    
    expect(res.status).toHaveBeenCalledWith(500);
    expect(res.json).toHaveBeenCalledWith({ error: 'Internal Server Error' });
  });
});

最佳实践 #

1. 使用测试数据库 #

javascript
beforeAll(async () => {
  await mongoose.connect(process.env.TEST_DB_URL);
});

afterAll(async () => {
  await mongoose.connection.close();
});

2. 清理测试数据 #

javascript
beforeEach(async () => {
  await User.deleteMany({});
});

3. Mock 外部依赖 #

javascript
jest.mock('axios');
jest.mock('./external-service');

4. 测试错误情况 #

javascript
test('handles errors', async () => {
  await expect(service.throwingMethod()).rejects.toThrow();
});

下一步 #

现在你已经掌握了 Node.js 测试,接下来学习 最佳实践 总结测试经验!

最后更新:2026-03-28