testing-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing Best Practices

测试最佳实践

This skill provides comprehensive expert knowledge of testing Node.js/Express applications with emphasis on Jest and Supertest, test organization, mocking strategies, and achieving comprehensive test coverage.
本技能提供Node.js/Express应用测试的全面专业知识,重点涵盖Jest与Supertest的使用、测试组织、模拟策略以及实现全面测试覆盖率的方法。

Testing Framework Setup

测试框架搭建

Jest Installation and Configuration

Jest安装与配置

Install dependencies:
bash
npm install --save-dev jest supertest @types/jest
package.json configuration:
json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ],
    "testMatch": [
      "**/__tests__/**/*.js",
      "**/?(*.)+(spec|test).js"
    ]
  }
}
jest.config.js (advanced):
javascript
module.exports = {
  // Use Node.js test environment
  testEnvironment: 'node',

  // Test file patterns
  testMatch: [
    '**/__tests__/**/*.js',
    '**/*.test.js',
    '**/*.spec.js'
  ],

  // Coverage settings
  collectCoverageFrom: [
    'src/**/*.js',
    'routes/**/*.js',
    '!src/index.js', // Exclude entry point
    '!**/node_modules/**'
  ],

  // Coverage thresholds
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },

  // Setup files
  setupFilesAfterEnv: ['<rootDir>/test/setup.js'],

  // Clear mocks between tests
  clearMocks: true,

  // Verbose output
  verbose: true,

  // Timeout for tests
  testTimeout: 10000
};
安装依赖:
bash
npm install --save-dev jest supertest @types/jest
package.json配置:
json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ],
    "testMatch": [
      "**/__tests__/**/*.js",
      "**/?(*.)+(spec|test).js"
    ]
  }
}
jest.config.js(进阶配置):
javascript
module.exports = {
  // 使用Node.js测试环境
  testEnvironment: 'node',

  // 测试文件匹配规则
  testMatch: [
    '**/__tests__/**/*.js',
    '**/*.test.js',
    '**/*.spec.js'
  ],

  // 覆盖率统计范围
  collectCoverageFrom: [
    'src/**/*.js',
    'routes/**/*.js',
    '!src/index.js', // 排除入口文件
    '!**/node_modules/**'
  ],

  // 覆盖率阈值
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },

  // 测试启动文件
  setupFilesAfterEnv: ['<rootDir>/test/setup.js'],

  // 测试间清除模拟
  clearMocks: true,

  // 详细输出
  verbose: true,

  // 测试超时时间
  testTimeout: 10000
};

Test Directory Structure

测试目录结构

Option 1: Separate test directory:
project/
├── src/
│   ├── server.js
│   ├── routes/
│   │   └── api.js
│   └── utils/
│       └── validators.js
├── test/
│   ├── setup.js
│   ├── server.test.js
│   ├── routes/
│   │   └── api.test.js
│   └── utils/
│       └── validators.test.js
└── package.json
Option 2: Co-located tests:
project/
├── src/
│   ├── server.js
│   ├── server.test.js
│   ├── routes/
│   │   ├── api.js
│   │   └── api.test.js
│   └── utils/
│       ├── validators.js
│       └── validators.test.js
└── package.json
Option 3: tests directories:
project/
├── src/
│   ├── __tests__/
│   │   └── server.test.js
│   ├── server.js
│   ├── routes/
│   │   ├── __tests__/
│   │   │   └── api.test.js
│   │   └── api.js
└── package.json
方案1:独立测试目录:
project/
├── src/
│   ├── server.js
│   ├── routes/
│   │   └── api.js
│   └── utils/
│       └── validators.js
├── test/
│   ├── setup.js
│   ├── server.test.js
│   ├── routes/
│   │   └── api.test.js
│   └── utils/
│       └── validators.test.js
└── package.json
方案2:测试文件与源码同目录:
project/
├── src/
│   ├── server.js
│   ├── server.test.js
│   ├── routes/
│   │   ├── api.js
│   │   └── api.test.js
│   └── utils/
│       ├── validators.js
│       └── validators.test.js
└── package.json
方案3:__tests__目录:
project/
├── src/
│   ├── __tests__/
│   │   └── server.test.js
│   ├── server.js
│   ├── routes/
│   │   ├── __tests__/
│   │   │   └── api.test.js
│   │   └── api.js
└── package.json

Testing Express Applications with Supertest

使用Supertest测试Express应用

Basic API Testing

基础API测试

javascript
const request = require('supertest');
const app = require('../server');

describe('GET /', () => {
  it('should return 200 status', async () => {
    const response = await request(app).get('/');
    expect(response.status).toBe(200);
  });

  it('should return JSON content type', async () => {
    const response = await request(app).get('/api/users');
    expect(response.headers['content-type']).toMatch(/json/);
  });

  it('should return users array', async () => {
    const response = await request(app).get('/api/users');
    expect(response.body).toHaveProperty('users');
    expect(Array.isArray(response.body.users)).toBe(true);
  });
});

describe('POST /api/users', () => {
  it('should create a user with valid data', async () => {
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'SecurePass123!'
    };

    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .set('Content-Type', 'application/json')
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe(userData.email);
    expect(response.body).not.toHaveProperty('password'); // Don't return password
  });

  it('should reject invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'invalid-email',
        password: 'SecurePass123!'
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/email/i);
  });

  it('should reject weak password', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'john@example.com',
        password: '123' // Too short
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/password/i);
  });
});

describe('Authentication', () => {
  let authToken;

  beforeAll(async () => {
    // Create a test user and get token
    const response = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'TestPass123!'
      });

    authToken = response.body.token;
  });

  it('should access protected route with valid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body).toHaveProperty('user');
  });

  it('should reject access without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });

  it('should reject invalid token', async () => {
    await request(app)
      .get('/api/profile')
      .set('Authorization', 'Bearer invalid-token')
      .expect(401);
  });
});
javascript
const request = require('supertest');
const app = require('../server');

describe('GET /', () => {
  it('should return 200 status', async () => {
    const response = await request(app).get('/');
    expect(response.status).toBe(200);
  });

  it('should return JSON content type', async () => {
    const response = await request(app).get('/api/users');
    expect(response.headers['content-type']).toMatch(/json/);
  });

  it('should return users array', async () => {
    const response = await request(app).get('/api/users');
    expect(response.body).toHaveProperty('users');
    expect(Array.isArray(response.body.users)).toBe(true);
  });
});

describe('POST /api/users', () => {
  it('should create a user with valid data', async () => {
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'SecurePass123!'
    };

    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .set('Content-Type', 'application/json')
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe(userData.email);
    expect(response.body).not.toHaveProperty('password'); // 不返回密码
  });

  it('should reject invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'invalid-email',
        password: 'SecurePass123!'
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/email/i);
  });

  it('should reject weak password', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'john@example.com',
        password: '123' // 过短
      })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toMatch(/password/i);
  });
});

describe('Authentication', () => {
  let authToken;

  beforeAll(async () => {
    // 创建测试用户并获取令牌
    const response = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'TestPass123!'
      });

    authToken = response.body.token;
  });

  it('should access protected route with valid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body).toHaveProperty('user');
  });

  it('should reject access without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });

  it('should reject invalid token', async () => {
    await request(app)
      .get('/api/profile')
      .set('Authorization', 'Bearer invalid-token')
      .expect(401);
  });
});

Testing Proxy Endpoints

测试代理端点

javascript
const request = require('supertest');
const axios = require('axios');
const app = require('../server');

// Mock axios
jest.mock('axios');

describe('POST /api/proxy', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should proxy request successfully', async () => {
    const mockData = {
      results: [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' }
      ]
    };

    axios.post.mockResolvedValue({
      data: mockData,
      status: 200
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(200);

    expect(response.body).toEqual(mockData);
    expect(axios.post).toHaveBeenCalledWith(
      expect.any(String),
      { query: 'test' },
      expect.any(Object)
    );
  });

  it('should handle proxy errors', async () => {
    axios.post.mockRejectedValue({
      response: {
        status: 500,
        data: { error: 'Internal Server Error' }
      }
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should handle network errors', async () => {
    axios.post.mockRejectedValue(new Error('Network error'));

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should validate request before proxying', async () => {
    const response = await request(app)
      .post('/api/proxy')
      .send({ invalid: 'data' })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(axios.post).not.toHaveBeenCalled();
  });
});
javascript
const request = require('supertest');
const axios = require('axios');
const app = require('../server');

// 模拟axios
jest.mock('axios');

describe('POST /api/proxy', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should proxy request successfully', async () => {
    const mockData = {
      results: [
        { id: 1, name: 'Result 1' },
        { id: 2, name: 'Result 2' }
      ]
    };

    axios.post.mockResolvedValue({
      data: mockData,
      status: 200
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(200);

    expect(response.body).toEqual(mockData);
    expect(axios.post).toHaveBeenCalledWith(
      expect.any(String),
      { query: 'test' },
      expect.any(Object)
    );
  });

  it('should handle proxy errors', async () => {
    axios.post.mockRejectedValue({
      response: {
        status: 500,
        data: { error: 'Internal Server Error' }
      }
    });

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should handle network errors', async () => {
    axios.post.mockRejectedValue(new Error('Network error'));

    const response = await request(app)
      .post('/api/proxy')
      .send({ query: 'test' })
      .expect(500);

    expect(response.body).toHaveProperty('error');
  });

  it('should validate request before proxying', async () => {
    const response = await request(app)
      .post('/api/proxy')
      .send({ invalid: 'data' })
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(axios.post).not.toHaveBeenCalled();
  });
});

Mocking Strategies

模拟策略

Mocking External APIs

模拟外部API

Mock entire module:
javascript
jest.mock('axios');

const axios = require('axios');

describe('External API calls', () => {
  it('should fetch data from external API', async () => {
    const mockData = { data: 'test' };
    axios.get.mockResolvedValue({ data: mockData });

    const result = await fetchExternalData();

    expect(result).toEqual(mockData);
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data');
  });
});
Mock specific functions:
javascript
const userService = require('../services/user');

jest.spyOn(userService, 'findById').mockResolvedValue({
  id: 1,
  name: 'Test User'
});

describe('User routes', () => {
  it('should get user by id', async () => {
    const response = await request(app)
      .get('/api/users/1')
      .expect(200);

    expect(response.body.name).toBe('Test User');
    expect(userService.findById).toHaveBeenCalledWith('1');
  });
});
Manual mocks:
javascript
// __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  put: jest.fn(() => Promise.resolve({ data: {} })),
  delete: jest.fn(() => Promise.resolve({ data: {} }))
};
模拟整个模块:
javascript
jest.mock('axios');

const axios = require('axios');

describe('External API calls', () => {
  it('should fetch data from external API', async () => {
    const mockData = { data: 'test' };
    axios.get.mockResolvedValue({ data: mockData });

    const result = await fetchExternalData();

    expect(result).toEqual(mockData);
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data');
  });
});
模拟特定函数:
javascript
const userService = require('../services/user');

jest.spyOn(userService, 'findById').mockResolvedValue({
  id: 1,
  name: 'Test User'
});

describe('User routes', () => {
  it('should get user by id', async () => {
    const response = await request(app)
      .get('/api/users/1')
      .expect(200);

    expect(response.body.name).toBe('Test User');
    expect(userService.findById).toHaveBeenCalledWith('1');
  });
});
手动模拟:
javascript
// __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  put: jest.fn(() => Promise.resolve({ data: {} })),
  delete: jest.fn(() => Promise.resolve({ data: {} }))
};

Mocking Database

模拟数据库

javascript
// Mock database module
jest.mock('../db');

const db = require('../db');

describe('Database operations', () => {
  beforeEach(() => {
    db.query.mockClear();
  });

  it('should query users', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' }
    ];

    db.query.mockResolvedValue({ rows: mockUsers });

    const users = await User.findAll();

    expect(users).toEqual(mockUsers);
    expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
  });

  it('should handle database errors', async () => {
    db.query.mockRejectedValue(new Error('Connection failed'));

    await expect(User.findAll()).rejects.toThrow('Connection failed');
  });
});
javascript
// 模拟数据库模块
jest.mock('../db');

const db = require('../db');

describe('Database operations', () => {
  beforeEach(() => {
    db.query.mockClear();
  });

  it('should query users', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' }
    ];

    db.query.mockResolvedValue({ rows: mockUsers });

    const users = await User.findAll();

    expect(users).toEqual(mockUsers);
    expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
  });

  it('should handle database errors', async () => {
    db.query.mockRejectedValue(new Error('Connection failed'));

    await expect(User.findAll()).rejects.toThrow('Connection failed');
  });
});

Mocking Environment Variables

模拟环境变量

javascript
describe('Environment configuration', () => {
  const originalEnv = process.env;

  beforeEach(() => {
    jest.resetModules();
    process.env = { ...originalEnv };
  });

  afterAll(() => {
    process.env = originalEnv;
  });

  it('should use default port when PORT not set', () => {
    delete process.env.PORT;
    const config = require('../config');
    expect(config.port).toBe(3000);
  });

  it('should use PORT from environment', () => {
    process.env.PORT = '8080';
    const config = require('../config');
    expect(config.port).toBe(8080);
  });
});
javascript
describe('Environment configuration', () => {
  const originalEnv = process.env;

  beforeEach(() => {
    jest.resetModules();
    process.env = { ...originalEnv };
  });

  afterAll(() => {
    process.env = originalEnv;
  });

  it('should use default port when PORT not set', () => {
    delete process.env.PORT;
    const config = require('../config');
    expect(config.port).toBe(3000);
  });

  it('should use PORT from environment', () => {
    process.env.PORT = '8080';
    const config = require('../config');
    expect(config.port).toBe(8080);
  });
});

Unit vs Integration vs E2E Testing

单元测试vs集成测试vs端到端测试

Unit Tests

单元测试

What: Test individual functions/modules in isolation
Example:
javascript
// validators.js
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function isStrongPassword(password) {
  return password.length >= 12 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password) &&
    /[^A-Za-z0-9]/.test(password);
}

module.exports = { isValidEmail, isStrongPassword };

// validators.test.js
const { isValidEmail, isStrongPassword } = require('./validators');

describe('Email validation', () => {
  it('should accept valid email', () => {
    expect(isValidEmail('test@example.com')).toBe(true);
  });

  it('should reject email without @', () => {
    expect(isValidEmail('testexample.com')).toBe(false);
  });

  it('should reject email without domain', () => {
    expect(isValidEmail('test@')).toBe(false);
  });

  it('should reject email with spaces', () => {
    expect(isValidEmail('test @example.com')).toBe(false);
  });
});

describe('Password validation', () => {
  it('should accept strong password', () => {
    expect(isStrongPassword('MyP@ssw0rd123!')).toBe(true);
  });

  it('should reject short password', () => {
    expect(isStrongPassword('Short1!')).toBe(false);
  });

  it('should reject password without uppercase', () => {
    expect(isStrongPassword('myp@ssw0rd123!')).toBe(false);
  });

  it('should reject password without special char', () => {
    expect(isStrongPassword('MyPassword123')).toBe(false);
  });
});
定义: 孤立测试单个函数/模块
示例:
javascript
// validators.js
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function isStrongPassword(password) {
  return password.length >= 12 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password) &&
    /[^A-Za-z0-9]/.test(password);
}

module.exports = { isValidEmail, isStrongPassword };

// validators.test.js
const { isValidEmail, isStrongPassword } = require('./validators');

describe('Email validation', () => {
  it('should accept valid email', () => {
    expect(isValidEmail('test@example.com')).toBe(true);
  });

  it('should reject email without @', () => {
    expect(isValidEmail('testexample.com')).toBe(false);
  });

  it('should reject email without domain', () => {
    expect(isValidEmail('test@')).toBe(false);
  });

  it('should reject email with spaces', () => {
    expect(isValidEmail('test @example.com')).toBe(false);
  });
});

describe('Password validation', () => {
  it('should accept strong password', () => {
    expect(isStrongPassword('MyP@ssw0rd123!')).toBe(true);
  });

  it('should reject short password', () => {
    expect(isStrongPassword('Short1!')).toBe(false);
  });

  it('should reject password without uppercase', () => {
    expect(isStrongPassword('myp@ssw0rd123!')).toBe(false);
  });

  it('should reject password without special char', () => {
    expect(isStrongPassword('MyPassword123')).toBe(false);
  });
});

Integration Tests

集成测试

What: Test multiple components working together
Example:
javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('User registration flow', () => {
  beforeEach(async () => {
    // Clean database before each test
    await db.query('DELETE FROM users');
  });

  it('should register user and allow login', async () => {
    // Register user
    const registerResponse = await request(app)
      .post('/api/register')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      })
      .expect(201);

    expect(registerResponse.body).toHaveProperty('id');

    // Login with registered credentials
    const loginResponse = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!'
      })
      .expect(200);

    expect(loginResponse.body).toHaveProperty('token');

    // Access protected route with token
    const profileResponse = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${loginResponse.body.token}`)
      .expect(200);

    expect(profileResponse.body.email).toBe('test@example.com');
  });
});
定义: 测试多个组件协同工作
示例:
javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('User registration flow', () => {
  beforeEach(async () => {
    // 每次测试前清理数据库
    await db.query('DELETE FROM users');
  });

  it('should register user and allow login', async () => {
    // 注册用户
    const registerResponse = await request(app)
      .post('/api/register')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      })
      .expect(201);

    expect(registerResponse.body).toHaveProperty('id');

    // 使用注册凭证登录
    const loginResponse = await request(app)
      .post('/api/login')
      .send({
        email: 'test@example.com',
        password: 'SecurePass123!'
      })
      .expect(200);

    expect(loginResponse.body).toHaveProperty('token');

    // 使用令牌访问受保护路由
    const profileResponse = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${loginResponse.body.token}`)
      .expect(200);

    expect(profileResponse.body.email).toBe('test@example.com');
  });
});

End-to-End (E2E) Tests

端到端(E2E)测试

What: Test complete user workflows from UI to database
Setup with Puppeteer:
bash
npm install --save-dev puppeteer
Example:
javascript
const puppeteer = require('puppeteer');

describe('E2E: User registration', () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox']
    });
    page = await browser.newPage();
  });

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

  it('should complete registration flow', async () => {
    // Navigate to registration page
    await page.goto('http://localhost:3000/register');

    // Fill out form
    await page.type('#email', 'test@example.com');
    await page.type('#password', 'SecurePass123!');
    await page.type('#confirmPassword', 'SecurePass123!');

    // Submit form
    await page.click('button[type="submit"]');

    // Wait for redirect to dashboard
    await page.waitForNavigation();

    // Verify we're on dashboard
    const url = page.url();
    expect(url).toContain('/dashboard');

    // Verify welcome message
    const welcomeMessage = await page.$eval(
      '.welcome',
      el => el.textContent
    );
    expect(welcomeMessage).toContain('test@example.com');
  });
});
定义: 测试从UI到数据库的完整用户工作流
使用Puppeteer搭建:
bash
npm install --save-dev puppeteer
示例:
javascript
const puppeteer = require('puppeteer');

describe('E2E: User registration', () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox']
    });
    page = await browser.newPage();
  });

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

  it('should complete registration flow', async () => {
    // 导航到注册页面
    await page.goto('http://localhost:3000/register');

    // 填写表单
    await page.type('#email', 'test@example.com');
    await page.type('#password', 'SecurePass123!');
    await page.type('#confirmPassword', 'SecurePass123!');

    // 提交表单
    await page.click('button[type="submit"]');

    // 等待重定向到仪表盘
    await page.waitForNavigation();

    // 验证是否在仪表盘页面
    const url = page.url();
    expect(url).toContain('/dashboard');

    // 验证欢迎消息
    const welcomeMessage = await page.$eval(
      '.welcome',
      el => el.textContent
    );
    expect(welcomeMessage).toContain('test@example.com');
  });
});

Test Organization

测试组织

Describe Blocks

Describe块

javascript
describe('User API', () => {
  describe('GET /api/users', () => {
    it('should return all users', async () => {
      // Test implementation
    });

    it('should support pagination', async () => {
      // Test implementation
    });

    it('should support filtering', async () => {
      // Test implementation
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      // Test implementation
    });

    it('should reject duplicate email', async () => {
      // Test implementation
    });
  });

  describe('PUT /api/users/:id', () => {
    it('should update user', async () => {
      // Test implementation
    });

    it('should reject unauthorized update', async () => {
      // Test implementation
    });
  });
});
javascript
describe('User API', () => {
  describe('GET /api/users', () => {
    it('should return all users', async () => {
      // 测试实现
    });

    it('should support pagination', async () => {
      // 测试实现
    });

    it('should support filtering', async () => {
      // 测试实现
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      // 测试实现
    });

    it('should reject duplicate email', async () => {
      // 测试实现
    });
  });

  describe('PUT /api/users/:id', () => {
    it('should update user', async () => {
      // 测试实现
    });

    it('should reject unauthorized update', async () => {
      // 测试实现
    });
  });
});

Setup and Teardown

前置与后置操作

javascript
describe('Database tests', () => {
  // Runs once before all tests in this describe block
  beforeAll(async () => {
    await db.connect();
  });

  // Runs once after all tests in this describe block
  afterAll(async () => {
    await db.disconnect();
  });

  // Runs before each test in this describe block
  beforeEach(async () => {
    await db.query('DELETE FROM users');
    await db.query('INSERT INTO users (email) VALUES ($1)', ['test@example.com']);
  });

  // Runs after each test in this describe block
  afterEach(async () => {
    jest.clearAllMocks();
  });

  it('should find user', async () => {
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeTruthy();
  });

  it('should delete user', async () => {
    await User.deleteByEmail('test@example.com');
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeNull();
  });
});
javascript
describe('Database tests', () => {
  // 在当前describe块的所有测试前运行一次
  beforeAll(async () => {
    await db.connect();
  });

  // 在当前describe块的所有测试后运行一次
  afterAll(async () => {
    await db.disconnect();
  });

  // 在当前describe块的每个测试前运行
  beforeEach(async () => {
    await db.query('DELETE FROM users');
    await db.query('INSERT INTO users (email) VALUES ($1)', ['test@example.com']);
  });

  // 在当前describe块的每个测试后运行
  afterEach(async () => {
    jest.clearAllMocks();
  });

  it('should find user', async () => {
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeTruthy();
  });

  it('should delete user', async () => {
    await User.deleteByEmail('test@example.com');
    const user = await User.findByEmail('test@example.com');
    expect(user).toBeNull();
  });
});

Test Fixtures

测试夹具

javascript
// test/fixtures/users.js
module.exports = {
  validUser: {
    email: 'test@example.com',
    password: 'SecurePass123!',
    name: 'Test User'
  },

  adminUser: {
    email: 'admin@example.com',
    password: 'AdminPass123!',
    name: 'Admin User',
    role: 'admin'
  },

  invalidUsers: {
    noEmail: {
      password: 'SecurePass123!',
      name: 'Test User'
    },
    weakPassword: {
      email: 'test@example.com',
      password: '123',
      name: 'Test User'
    }
  }
};

// Usage in tests
const fixtures = require('./fixtures/users');

describe('User creation', () => {
  it('should create valid user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.validUser)
      .expect(201);
  });

  it('should reject user without email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.invalidUsers.noEmail)
      .expect(400);
  });
});
javascript
// test/fixtures/users.js
module.exports = {
  validUser: {
    email: 'test@example.com',
    password: 'SecurePass123!',
    name: 'Test User'
  },

  adminUser: {
    email: 'admin@example.com',
    password: 'AdminPass123!',
    name: 'Admin User',
    role: 'admin'
  },

  invalidUsers: {
    noEmail: {
      password: 'SecurePass123!',
      name: 'Test User'
    },
    weakPassword: {
      email: 'test@example.com',
      password: '123',
      name: 'Test User'
    }
  }
};

// 在测试中使用
const fixtures = require('./fixtures/users');

describe('User creation', () => {
  it('should create valid user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.validUser)
      .expect(201);
  });

  it('should reject user without email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send(fixtures.invalidUsers.noEmail)
      .expect(400);
  });
});

Async Testing

异步测试

Testing Promises

测试Promise

javascript
describe('Async operations', () => {
  it('should resolve with data', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
  });

  it('should reject with error', async () => {
    await expect(fetchInvalidData()).rejects.toThrow('Not found');
  });

  // Alternative: using done callback
  it('should fetch data (callback style)', (done) => {
    fetchData()
      .then(data => {
        expect(data).toBeDefined();
        done();
      })
      .catch(done);
  });
});
javascript
describe('Async operations', () => {
  it('should resolve with data', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
  });

  it('should reject with error', async () => {
    await expect(fetchInvalidData()).rejects.toThrow('Not found');
  });

  // 替代方案:使用done回调
  it('should fetch data (callback style)', (done) => {
    fetchData()
      .then(data => {
        expect(data).toBeDefined();
        done();
      })
      .catch(done);
  });
});

Testing Callbacks

测试回调函数

javascript
describe('Callback functions', () => {
  it('should call callback with data', (done) => {
    fetchDataWithCallback((err, data) => {
      expect(err).toBeNull();
      expect(data).toBeDefined();
      done();
    });
  });

  it('should call callback with error', (done) => {
    fetchInvalidDataWithCallback((err, data) => {
      expect(err).toBeTruthy();
      expect(data).toBeUndefined();
      done();
    });
  });
});
javascript
describe('Callback functions', () => {
  it('should call callback with data', (done) => {
    fetchDataWithCallback((err, data) => {
      expect(err).toBeNull();
      expect(data).toBeDefined();
      done();
    });
  });

  it('should call callback with error', (done) => {
    fetchInvalidDataWithCallback((err, data) => {
      expect(err).toBeTruthy();
      expect(data).toBeUndefined();
      done();
    });
  });
});

Code Coverage

代码覆盖率

Generating Coverage Reports

生成覆盖率报告

bash
undefined
bash
undefined

Run tests with coverage

带覆盖率运行测试

npm run test:coverage
npm run test:coverage

Coverage report output

覆盖率报告输出

----------|---------|----------|---------|---------|
File% Stmts% Branch% Funcs% Lines
All files85.578.391.285.1
server.js92.385.710091.8
routes/78.971.483.379.2
-----------------------------------------------
undefined
----------|---------|----------|---------|---------|
File% Stmts% Branch% Funcs% Lines
All files85.578.391.285.1
server.js92.385.710091.8
routes/78.971.483.379.2
-----------------------------------------------
undefined

Coverage Configuration

覆盖率配置

javascript
// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/index.js', // Exclude entry point
    '!src/**/*.test.js', // Exclude test files
    '!src/**/__tests__/**' // Exclude test directories
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    // Per-file thresholds
    './src/critical-module.js': {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100
    }
  },

  coverageReporters: [
    'text',      // Terminal output
    'html',      // HTML report in coverage/
    'lcov',      // For CI tools
    'json'       // Machine-readable
  ]
};
javascript
// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/index.js', // 排除入口文件
    '!src/**/*.test.js', // 排除测试文件
    '!src/**/__tests__/**' // 排除测试目录
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    // 按文件设置阈值
    './src/critical-module.js': {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100
    }
  },

  coverageReporters: [
    'text',      // 终端输出
    'html',      // 生成HTML报告在coverage/目录
    'lcov',      // 用于CI工具
    'json'       // 机器可读格式
  ]
};

Viewing HTML Coverage Report

查看HTML覆盖率报告

bash
npm run test:coverage
open coverage/index.html  # macOS
xdg-open coverage/index.html  # Linux
start coverage/index.html  # Windows
bash
npm run test:coverage
open coverage/index.html  # macOS
xdg-open coverage/index.html  # Linux
start coverage/index.html  # Windows

Testing Best Practices

测试最佳实践

1. Naming Conventions

1. 命名规范

javascript
// GOOD - Descriptive test names
describe('User registration', () => {
  it('should create user with valid email and password', () => {});
  it('should reject registration with duplicate email', () => {});
  it('should hash password before storing', () => {});
});

// BAD - Vague test names
describe('User', () => {
  it('works', () => {});
  it('test 1', () => {});
  it('should not fail', () => {});
});
javascript
// 良好 - 描述性测试名称
describe('User registration', () => {
  it('should create user with valid email and password', () => {});
  it('should reject registration with duplicate email', () => {});
  it('should hash password before storing', () => {});
});

// 糟糕 - 模糊的测试名称
describe('User', () => {
  it('works', () => {});
  it('test 1', () => {});
  it('should not fail', () => {});
});

2. AAA Pattern (Arrange, Act, Assert)

2. AAA模式(准备、执行、断言)

javascript
it('should calculate total price with tax', () => {
  // Arrange: Set up test data
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 }
  ];
  const taxRate = 0.1;

  // Act: Perform the action
  const total = calculateTotal(items, taxRate);

  // Assert: Verify the result
  expect(total).toBe(38.5); // (10*2 + 5*3) * 1.1
});
javascript
it('should calculate total price with tax', () => {
  // Arrange: 设置测试数据
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 }
  ];
  const taxRate = 0.1;

  // Act: 执行操作
  const total = calculateTotal(items, taxRate);

  // Assert: 验证结果
  expect(total).toBe(38.5); // (10*2 + 5*3) * 1.1
});

3. Test One Thing

3. 单一测试原则

javascript
// GOOD - Each test checks one behavior
it('should validate email format', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

it('should reject email without domain', () => {
  expect(isValidEmail('test@')).toBe(false);
});

// BAD - Testing multiple things
it('should validate inputs', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidPassword('pass123')).toBe(false);
  expect(isValidPhone('1234567890')).toBe(true);
});
javascript
// 良好 - 每个测试检查一个行为
it('should validate email format', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

it('should reject email without domain', () => {
  expect(isValidEmail('test@')).toBe(false);
});

// 糟糕 - 测试多个内容
it('should validate inputs', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidPassword('pass123')).toBe(false);
  expect(isValidPhone('1234567890')).toBe(true);
});

4. Avoid Test Interdependence

4. 避免测试依赖

javascript
// BAD - Tests depend on each other
let userId;

it('should create user', async () => {
  const response = await createUser();
  userId = response.id; // Other tests depend on this
});

it('should update user', async () => {
  await updateUser(userId); // Fails if previous test fails
});

// GOOD - Each test is independent
describe('User operations', () => {
  let userId;

  beforeEach(async () => {
    const user = await createUser();
    userId = user.id;
  });

  it('should update user', async () => {
    await updateUser(userId);
  });

  it('should delete user', async () => {
    await deleteUser(userId);
  });
});
javascript
// 糟糕 - 测试相互依赖
let userId;

it('should create user', async () => {
  const response = await createUser();
  userId = response.id; // 其他测试依赖此值
});

it('should update user', async () => {
  await updateUser(userId); // 如果前一个测试失败,此测试也会失败
});

// 良好 - 每个测试独立
describe('User operations', () => {
  let userId;

  beforeEach(async () => {
    const user = await createUser();
    userId = user.id;
  });

  it('should update user', async () => {
    await updateUser(userId);
  });

  it('should delete user', async () => {
    await deleteUser(userId);
  });
});

5. Use Meaningful Assertions

5. 使用有意义的断言

javascript
// GOOD - Specific assertions
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(5);
expect(response.body.users[0]).toMatchObject({
  id: expect.any(Number),
  email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
});

// BAD - Vague assertions
expect(response).toBeTruthy();
expect(response.body).toBeDefined();
javascript
// 良好 - 具体的断言
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(5);
expect(response.body.users[0]).toMatchObject({
  id: expect.any(Number),
  email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
});

// 糟糕 - 模糊的断言
expect(response).toBeTruthy();
expect(response.body).toBeDefined();

6. Test Edge Cases

6. 测试边缘情况

javascript
describe('Division function', () => {
  it('should divide positive numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  it('should handle zero numerator', () => {
    expect(divide(0, 5)).toBe(0);
  });

  it('should throw error for division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });

  it('should handle decimal results', () => {
    expect(divide(5, 2)).toBe(2.5);
  });
});
javascript
describe('Division function', () => {
  it('should divide positive numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  it('should handle zero numerator', () => {
    expect(divide(0, 5)).toBe(0);
  });

  it('should throw error for division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });

  it('should handle decimal results', () => {
    expect(divide(5, 2)).toBe(2.5);
  });
});

CI/CD Integration

CI/CD集成

GitHub Actions

GitHub Actions

yaml
undefined
yaml
undefined

.github/workflows/test.yml

.github/workflows/test.yml

name: Tests
on: push: branches: [ main, develop ] pull_request: branches: [ main ]
jobs: test: runs-on: ubuntu-latest
strategy:
  matrix:
    node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v3

- name: Use Node.js ${{ matrix.node-version }}
  uses: actions/setup-node@v3
  with:
    node-version: ${{ matrix.node-version }}

- name: Install dependencies
  run: npm ci

- name: Run tests
  run: npm test

- name: Generate coverage report
  run: npm run test:coverage

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    file: ./coverage/coverage-final.json
    fail_ci_if_error: true
undefined
name: Tests
on: push: branches: [ main, develop ] pull_request: branches: [ main ]
jobs: test: runs-on: ubuntu-latest
strategy:
  matrix:
    node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v3

- name: Use Node.js ${{ matrix.node-version }}
  uses: actions/setup-node@v3
  with:
    node-version: ${{ matrix.node-version }}

- name: Install dependencies
  run: npm ci

- name: Run tests
  run: npm test

- name: Generate coverage report
  run: npm run test:coverage

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    file: ./coverage/coverage-final.json
    fail_ci_if_error: true
undefined

npm Scripts for CI

用于CI的npm脚本

json
{
  "scripts": {
    "test": "jest",
    "test:ci": "jest --ci --coverage --maxWorkers=2",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}
json
{
  "scripts": {
    "test": "jest",
    "test:ci": "jest --ci --coverage --maxWorkers=2",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}

Common Jest Matchers

常用Jest匹配器

Equality

相等性

javascript
expect(value).toBe(4); // Strict equality (===)
expect(value).toEqual({ a: 1 }); // Deep equality
expect(value).not.toBe(5); // Negation
javascript
expect(value).toBe(4); // 严格相等 (===)
expect(value).toEqual({ a: 1 }); // 深度相等
expect(value).not.toBe(5); // 取反

Truthiness

真值判断

javascript
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
javascript
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

Numbers

数字

javascript
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(0.3); // Floating point
javascript
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(0.3); // 浮点数比较

Strings

字符串

javascript
expect(string).toMatch(/pattern/);
expect(string).toMatch('substring');
expect(string).toContain('substring');
javascript
expect(string).toMatch(/pattern/);
expect(string).toMatch('substring');
expect(string).toContain('substring');

Arrays and Iterables

数组与可迭代对象

javascript
expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));
javascript
expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));

Objects

对象

javascript
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', value);
expect(object).toMatchObject({ a: 1, b: 2 });
expect(object).toEqual(expect.objectContaining({ a: 1 }));
javascript
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', value);
expect(object).toMatchObject({ a: 1, b: 2 });
expect(object).toEqual(expect.objectContaining({ a: 1 }));

Functions

函数

javascript
expect(fn).toThrow();
expect(fn).toThrow('error message');
expect(fn).toThrow(Error);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(3);
javascript
expect(fn).toThrow();
expect(fn).toThrow('error message');
expect(fn).toThrow(Error);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(3);

Testing Checklist

测试检查清单

Unit Tests

单元测试

  • Test pure functions in isolation
  • Test all code paths (happy path and error cases)
  • Test edge cases and boundary conditions
  • Mock external dependencies
  • Achieve high code coverage (>80%)
  • 孤立测试纯函数
  • 测试所有代码路径(正常流程和错误场景)
  • 测试边缘情况和边界条件
  • 模拟外部依赖
  • 实现高代码覆盖率(>80%)

Integration Tests

集成测试

  • Test API endpoints
  • Test authentication/authorization
  • Test database operations
  • Test external API integration
  • Test error handling
  • 测试API端点
  • 测试认证/授权
  • 测试数据库操作
  • 测试外部API集成
  • 测试错误处理

E2E Tests

端到端测试

  • Test critical user flows
  • Test form submissions
  • Test navigation
  • Test authentication flow
  • 测试关键用户流程
  • 测试表单提交
  • 测试导航
  • 测试认证流程

General

通用

  • Tests are fast (< 5 seconds for unit tests)
  • Tests are independent (can run in any order)
  • Tests are repeatable (same result every time)
  • Tests have clear, descriptive names
  • Setup and teardown properly implemented
  • No hardcoded values (use constants/fixtures)
  • CI/CD integration configured
  • 测试速度快(单元测试<5秒)
  • 测试独立(可按任意顺序运行)
  • 测试可重复(每次结果一致)
  • 测试名称清晰、具有描述性
  • 正确实现前置与后置操作
  • 无硬编码值(使用常量/夹具)
  • 配置CI/CD集成

Example Test Suite for Express API

Express API测试套件示例

javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('Express API Tests', () => {
  // Setup: Connect to test database
  beforeAll(async () => {
    await db.connect(process.env.TEST_DATABASE_URL);
  });

  // Cleanup: Disconnect from database
  afterAll(async () => {
    await db.disconnect();
  });

  // Reset database before each test
  beforeEach(async () => {
    await db.query('DELETE FROM users');
  });

  describe('GET /api/health', () => {
    it('should return health status', async () => {
      const response = await request(app)
        .get('/api/health')
        .expect(200);

      expect(response.body).toEqual({
        status: 'ok',
        timestamp: expect.any(Number)
      });
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(Number),
        email: userData.email,
        name: userData.name
      });
      expect(response.body).not.toHaveProperty('password');
    });

    it('should reject duplicate email', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      // Create first user
      await request(app).post('/api/users').send(userData);

      // Try to create duplicate
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(409);

      expect(response.body.error).toMatch(/already exists/i);
    });

    it('should validate email format', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'invalid-email',
          password: 'SecurePass123!',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/email/i);
    });

    it('should enforce password requirements', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'test@example.com',
          password: 'weak',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/password/i);
    });
  });

  describe('Authentication', () => {
    let authToken;
    const testUser = {
      email: 'auth@example.com',
      password: 'SecurePass123!',
      name: 'Auth User'
    };

    beforeEach(async () => {
      // Create user
      await request(app).post('/api/users').send(testUser);

      // Login and get token
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        });

      authToken = response.body.token;
    });

    it('should login with valid credentials', async () => {
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        })
        .expect(200);

      expect(response.body).toHaveProperty('token');
    });

    it('should reject invalid credentials', async () => {
      await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: 'WrongPassword'
        })
        .expect(401);
    });

    it('should access protected route with token', async () => {
      const response = await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.email).toBe(testUser.email);
    });

    it('should reject access without token', async () => {
      await request(app)
        .get('/api/profile')
        .expect(401);
    });
  });
});
javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');

describe('Express API Tests', () => {
  // 前置:连接测试数据库
  beforeAll(async () => {
    await db.connect(process.env.TEST_DATABASE_URL);
  });

  // 清理:断开数据库连接
  afterAll(async () => {
    await db.disconnect();
  });

  // 每次测试前重置数据库
  beforeEach(async () => {
    await db.query('DELETE FROM users');
  });

  describe('GET /api/health', () => {
    it('should return health status', async () => {
      const response = await request(app)
        .get('/api/health')
        .expect(200);

      expect(response.body).toEqual({
        status: 'ok',
        timestamp: expect.any(Number)
      });
    });
  });

  describe('POST /api/users', () => {
    it('should create user with valid data', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      expect(response.body).toMatchObject({
        id: expect.any(Number),
        email: userData.email,
        name: userData.name
      });
      expect(response.body).not.toHaveProperty('password');
    });

    it('should reject duplicate email', async () => {
      const userData = {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      };

      // 创建第一个用户
      await request(app).post('/api/users').send(userData);

      // 尝试创建重复用户
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(409);

      expect(response.body.error).toMatch(/already exists/i);
    });

    it('should validate email format', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'invalid-email',
          password: 'SecurePass123!',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/email/i);
    });

    it('should enforce password requirements', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({
          email: 'test@example.com',
          password: 'weak',
          name: 'Test'
        })
        .expect(400);

      expect(response.body.error).toMatch(/password/i);
    });
  });

  describe('Authentication', () => {
    let authToken;
    const testUser = {
      email: 'auth@example.com',
      password: 'SecurePass123!',
      name: 'Auth User'
    };

    beforeEach(async () => {
      // 创建用户
      await request(app).post('/api/users').send(testUser);

      // 登录并获取令牌
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        });

      authToken = response.body.token;
    });

    it('should login with valid credentials', async () => {
      const response = await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: testUser.password
        })
        .expect(200);

      expect(response.body).toHaveProperty('token');
    });

    it('should reject invalid credentials', async () => {
      await request(app)
        .post('/api/login')
        .send({
          email: testUser.email,
          password: 'WrongPassword'
        })
        .expect(401);
    });

    it('should access protected route with token', async () => {
      const response = await request(app)
        .get('/api/profile')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.email).toBe(testUser.email);
    });

    it('should reject access without token', async () => {
      await request(app)
        .get('/api/profile')
        .expect(401);
    });
  });
});

Resources

资源