Skip to content
On this page

Node.js 测试策略

测试是确保Node.js应用质量和稳定性的关键环节。本章详细介绍Node.js中的各种测试策略和最佳实践。

测试类型

单元测试

javascript
// math.js - 被测试的模块
function add(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('参数必须是数字');
  }
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

function isEven(num) {
  return num % 2 === 0;
}

module.exports = { add, multiply, isEven };
javascript
// math.test.js - 单元测试示例 (使用Jest)
const { add, multiply, isEven } = require('./math');

describe('数学函数测试', () => {
  describe('add函数', () => {
    test('应该正确相加两个正数', () => {
      expect(add(2, 3)).toBe(5);
      expect(add(0, 0)).toBe(0);
      expect(add(-1, 1)).toBe(0);
    });

    test('应该处理小数', () => {
      expect(add(0.1, 0.2)).toBeCloseTo(0.3);
    });

    test('应该抛出非数字参数的错误', () => {
      expect(() => add('a', 'b')).toThrow('参数必须是数字');
      expect(() => add(1, 'b')).toThrow('参数必须是数字');
      expect(() => add(null, 2)).toThrow('参数必须是数字');
    });
  });

  describe('multiply函数', () => {
    test('应该正确相乘两个数', () => {
      expect(multiply(2, 3)).toBe(6);
      expect(multiply(0, 5)).toBe(0);
      expect(multiply(-2, 3)).toBe(-6);
    });
  });

  describe('isEven函数', () => {
    test('应该正确判断偶数', () => {
      expect(isEven(2)).toBe(true);
      expect(isEven(0)).toBe(true);
      expect(isEven(-2)).toBe(true);
    });

    test('应该正确判断奇数', () => {
      expect(isEven(1)).toBe(false);
      expect(isEven(3)).toBe(false);
      expect(isEven(-1)).toBe(false);
    });
  });
});

集成测试

javascript
// user-service.js
const { add } = require('./math');

class UserService {
  constructor(database) {
    this.db = database;
  }

  async createUser(userData) {
    if (!userData.name || !userData.email) {
      throw new Error('姓名和邮箱是必需的');
    }

    // 验证邮箱格式
    if (!this.isValidEmail(userData.email)) {
      throw new Error('无效的邮箱格式');
    }

    // 计算用户ID(使用数学函数)
    const id = add(Date.now(), userData.name.length);
    
    const user = {
      id,
      name: userData.name,
      email: userData.email,
      createdAt: new Date().toISOString()
    };

    await this.db.insert('users', user);
    return user;
  }

  async getUserById(id) {
    return await this.db.find('users', { id });
  }

  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

module.exports = UserService;
javascript
// user-service.integration.test.js
const UserService = require('./user-service');

// 模拟数据库
const mockDatabase = {
  insert: jest.fn(),
  find: jest.fn()
};

describe('UserService集成测试', () => {
  let userService;

  beforeEach(() => {
    userService = new UserService(mockDatabase);
    jest.clearAllMocks();
  });

  describe('createUser方法', () => {
    test('应该成功创建用户', async () => {
      const userData = {
        name: '张三',
        email: 'zhang@example.com'
      };

      mockDatabase.insert.mockResolvedValueOnce();

      const result = await userService.createUser(userData);

      expect(result).toHaveProperty('id');
      expect(result.name).toBe('张三');
      expect(result.email).toBe('zhang@example.com');
      expect(result).toHaveProperty('createdAt');
      
      expect(mockDatabase.insert).toHaveBeenCalledWith('users', expect.objectContaining({
        name: '张三',
        email: 'zhang@example.com'
      }));
    });

    test('应该验证邮箱格式', async () => {
      const invalidUserData = {
        name: '李四',
        email: 'invalid-email'
      };

      await expect(userService.createUser(invalidUserData))
        .rejects
        .toThrow('无效的邮箱格式');
    });

    test('应该验证必需字段', async () => {
      const incompleteUserData = {
        name: '王五'
        // 缺少email字段
      };

      await expect(userService.createUser(incompleteUserData))
        .rejects
        .toThrow('姓名和邮箱是必需的');
    });
  });

  describe('getUserById方法', () => {
    test('应该根据ID获取用户', async () => {
      const mockUser = {
        id: 123,
        name: '赵六',
        email: 'zhao@example.com'
      };

      mockDatabase.find.mockResolvedValueOnce(mockUser);

      const result = await userService.getUserById(123);

      expect(result).toEqual(mockUser);
      expect(mockDatabase.find).toHaveBeenCalledWith('users', { id: 123 });
    });
  });
});

测试框架

Jest测试框架

javascript
// 安装: npm install --save-dev jest

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[tj]s?(x)'
  ],
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js',
    '!src/**/*.spec.js'
  ]
};
javascript
// 异步测试示例
describe('异步操作测试', () => {
  // 模拟异步操作
  function asyncOperation(value, delay = 100) {
    return new Promise(resolve => {
      setTimeout(() => resolve(value), delay);
    });
  }

  test('应该等待异步操作完成', async () => {
    const result = await asyncOperation('test');
    expect(result).toBe('test');
  });

  test('应该处理异步错误', async () => {
    const failingAsyncOperation = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('异步错误')), 100);
      });
    };

    await expect(failingAsyncOperation()).rejects.toThrow('异步错误');
  });

  // 使用done回调
  test('应该使用done回调测试异步操作', (done) => {
    asyncOperation('callback-test').then(result => {
      expect(result).toBe('callback-test');
      done();
    });
  });
});

测试Mock和Spy

javascript
// api-client.js
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async get(endpoint) {
    const response = await fetch(`${this.baseURL}${endpoint}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  }

  async post(endpoint, data) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  }
}

module.exports = ApiClient;
javascript
// api-client.test.js
const ApiClient = require('./api-client');

describe('ApiClient测试', () => {
  let apiClient;
  let mockFetch;

  beforeEach(() => {
    mockFetch = jest.fn();
    global.fetch = mockFetch;
    apiClient = new ApiClient('https://api.example.com');
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  describe('get方法', () => {
    test('应该正确获取数据', async () => {
      const mockData = { id: 1, name: '测试' };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockData
      });

      const result = await apiClient.get('/users/1');

      expect(result).toEqual(mockData);
      expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/1');
    });

    test('应该处理HTTP错误', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        statusText: 'Not Found'
      });

      await expect(apiClient.get('/users/999'))
        .rejects
        .toThrow('HTTP 404: Not Found');
    });
  });

  describe('post方法', () => {
    test('应该正确发送POST请求', async () => {
      const testData = { name: '新用户' };
      const mockResponse = { id: 123, ...testData };

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse
      });

      const result = await apiClient.post('/users', testData);

      expect(result).toEqual(mockResponse);
      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.example.com/users',
        expect.objectContaining({
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(testData)
        })
      );
    });
  });
});

API测试

使用Supertest进行HTTP测试

bash
# 安装: npm install --save-dev supertest
javascript
// app.js
const express = require('express');
const app = express();

app.use(express.json());

let users = [
  { id: 1, name: '张三', email: 'zhang@example.com' },
  { id: 2, name: '李四', email: 'li@example.com' }
];

app.get('/api/users', (req, res) => {
  const { page = 1, limit = 10 } = req.query;
  const startIndex = (page - 1) * limit;
  const endIndex = page * limit;
  
  res.json({
    users: users.slice(startIndex, endIndex),
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total: users.length
    }
  });
});

app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: '用户不存在' });
  }
  res.json(user);
});

app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: '姓名和邮箱是必需的' });
  }
  
  const newUser = {
    id: users.length > 0 ? Math.max(...users.map(u => u.id)) + 1 : 1,
    name,
    email
  };
  
  users.push(newUser);
  res.status(201).json(newUser);
});

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

describe('API测试', () => {
  describe('GET /api/users', () => {
    test('应该返回用户列表', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect(200);
      
      expect(response.body).toHaveProperty('users');
      expect(response.body).toHaveProperty('pagination');
      expect(Array.isArray(response.body.users)).toBe(true);
      expect(response.body.users.length).toBeGreaterThan(0);
    });

    test('应该支持分页参数', async () => {
      const response = await request(app)
        .get('/api/users?page=1&limit=1')
        .expect(200);
      
      expect(response.body.pagination.page).toBe(1);
      expect(response.body.pagination.limit).toBe(1);
      expect(response.body.users.length).toBe(1);
    });
  });

  describe('GET /api/users/:id', () => {
    test('应该返回特定用户', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200);
      
      expect(response.body).toEqual({
        id: 1,
        name: '张三',
        email: 'zhang@example.com'
      });
    });

    test('应该返回404当用户不存在时', async () => {
      const response = await request(app)
        .get('/api/users/999')
        .expect(404);
      
      expect(response.body).toEqual({ error: '用户不存在' });
    });
  });

  describe('POST /api/users', () => {
    test('应该创建新用户', async () => {
      const newUser = {
        name: '王五',
        email: 'wang@example.com'
      };

      const response = await request(app)
        .post('/api/users')
        .send(newUser)
        .expect(201);
      
      expect(response.body).toHaveProperty('id');
      expect(response.body.name).toBe('王五');
      expect(response.body.email).toBe('wang@example.com');
    });

    test('应该验证必需字段', async () => {
      const invalidUser = {
        name: '赵六'
        // 缺少email
      };

      const response = await request(app)
        .post('/api/users')
        .send(invalidUser)
        .expect(400);
      
      expect(response.body).toEqual({ error: '姓名和邮箱是必需的' });
    });
  });
});

数据库测试

使用内存数据库进行测试

javascript
// database.js
const sqlite3 = require('sqlite3').verbose();

class Database {
  constructor(filename = ':memory:') {
    this.db = new sqlite3.Database(filename);
    this.init();
  }

  init() {
    this.db.run(`
      CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `);
  }

  async createUser(userData) {
    return new Promise((resolve, reject) => {
      this.db.run(
        'INSERT INTO users (name, email) VALUES (?, ?)',
        [userData.name, userData.email],
        function(err) {
          if (err) {
            reject(err);
          } else {
            resolve({ id: this.lastID, ...userData });
          }
        }
      );
    });
  }

  async getUserById(id) {
    return new Promise((resolve, reject) => {
      this.db.get(
        'SELECT * FROM users WHERE id = ?',
        [id],
        (err, row) => {
          if (err) {
            reject(err);
          } else {
            resolve(row);
          }
        }
      );
    });
  }

  close() {
    this.db.close();
  }
}

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

describe('Database测试', () => {
  let db;

  beforeEach(async () => {
    // 使用内存数据库进行测试
    db = new Database();
    
    // 插入测试数据
    await db.createUser({ name: '张三', email: 'zhang@example.com' });
    await db.createUser({ name: '李四', email: 'li@example.com' });
  });

  afterEach(() => {
    db.close();
  });

  test('应该创建用户', async () => {
    const newUser = await db.createUser({
      name: '王五',
      email: 'wang@example.com'
    });

    expect(newUser).toHaveProperty('id');
    expect(newUser.name).toBe('王五');
    expect(newUser.email).toBe('wang@example.com');
  });

  test('应该获取用户', async () => {
    const user = await db.getUserById(1);
    expect(user).toEqual({
      id: 1,
      name: '张三',
      email: 'zhang@example.com',
      // created_at 字段会被自动添加
    });
  });

  test('应该处理不存在的用户', async () => {
    const user = await db.getUserById(999);
    expect(user).toBeUndefined();
  });
});

测试覆盖率

配置Jest覆盖率

javascript
// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --maxWorkers=2"
  },
  "devDependencies": {
    "jest": "^28.0.0",
    "supertest": "^6.0.0"
  }
}
javascript
// 覆盖率测试示例
describe('分支覆盖率测试', () => {
  function processUser(user) {
    if (!user) {
      return { error: '用户不存在' }; // 分支1
    }
    
    if (!user.email) {
      return { error: '邮箱是必需的' }; // 分支2
    }
    
    if (!user.name) {
      return { error: '姓名是必需的' }; // 分支3
    }
    
    return { success: true, user }; // 分支4
  }

  test('应该测试所有分支', () => {
    // 测试分支1: user为null
    expect(processUser(null)).toEqual({ error: '用户不存在' });
    
    // 测试分支2: user存在但email为空
    expect(processUser({})).toEqual({ error: '邮箱是必需的' });
    
    // 测试分支3: user和email存在但name为空
    expect(processUser({ email: 'test@example.com' })).toEqual({ error: '姓名是必需的' });
    
    // 测试分支4: 所有条件都满足
    const validUser = { name: '测试', email: 'test@example.com' };
    expect(processUser(validUser)).toEqual({ success: true, user: validUser });
  });
});

测试最佳实践

测试组织结构

project/
├── src/
│   ├── user/
│   │   ├── user.service.js
│   │   ├── user.controller.js
│   │   └── user.model.js
│   └── auth/
│       ├── auth.service.js
│       └── auth.middleware.js
├── tests/
│   ├── unit/
│   │   ├── user/
│   │   │   └── user.service.test.js
│   │   └── auth/
│   │       └── auth.service.test.js
│   ├── integration/
│   │   ├── user/
│   │   │   └── user.api.test.js
│   │   └── auth/
│   │       └── auth.api.test.js
│   └── e2e/
│       └── user-flow.test.js
└── jest.config.js

测试数据工厂

javascript
// test-utils.js
function createUser(userData = {}) {
  return {
    name: userData.name || '测试用户',
    email: userData.email || `test${Date.now()}@example.com`,
    age: userData.age || 25,
    ...userData
  };
}

function createOrder(orderData = {}) {
  return {
    productId: orderData.productId || 1,
    quantity: orderData.quantity || 1,
    userId: orderData.userId || 1,
    status: orderData.status || 'pending',
    ...orderData
  };
}

module.exports = {
  createUser,
  createOrder
};

测试环境设置

javascript
// setup-test.js
const { createUser } = require('./test-utils');

// 全局测试设置
beforeAll(async () => {
  // 设置测试数据库
  // 启动测试服务器
});

beforeEach(async () => {
  // 清理测试数据
  // 重置mock
});

afterEach(async () => {
  // 清理资源
  jest.clearAllMocks();
});

afterAll(async () => {
  // 关闭数据库连接
  // 关闭测试服务器
});

// 重用测试数据
const TEST_USER = createUser({ name: 'Test User', email: 'test@example.com' });

性能测试

基准测试

javascript
// benchmark.test.js
const { performance } = require('perf_hooks');

describe('性能测试', () => {
  test('应该在合理时间内完成操作', async () => {
    const start = performance.now();
    
    // 执行需要测试的函数
    await someFunctionThatShouldBeFast();
    
    const end = performance.now();
    const duration = end - start;
    
    // 断言执行时间应该小于100毫秒
    expect(duration).toBeLessThan(100);
  });

  test('应该测试大数据集处理性能', () => {
    const largeDataset = Array.from({ length: 10000 }, (_, i) => i);
    const start = performance.now();
    
    // 处理大数据集
    const result = largeDataset.map(x => x * 2).filter(x => x > 100);
    
    const end = performance.now();
    const duration = end - start;
    
    console.log(`处理10000个元素耗时: ${duration}ms`);
    expect(duration).toBeLessThan(500); // 应该在500ms内完成
  });
});

通过全面的测试策略,可以确保Node.js应用的质量、稳定性和可维护性。