发布于

Jest 测试框架完全指南:JavaScript 单元测试与集成测试最佳实践

作者

Jest 测试框架完全指南:JavaScript 单元测试与集成测试最佳实践

Jest 是一个功能强大的 JavaScript 测试框架,提供了零配置的测试体验。本文将深入探讨 Jest 的核心特性、测试策略和最佳实践。

Jest 基础配置

项目初始化

// jest.config.js - Jest 配置文件
module.exports = {
  // 测试环境
  testEnvironment: 'node', // 或 'jsdom' 用于浏览器环境

  // 测试文件匹配模式
  testMatch: ['**/__tests__/**/*.(js|jsx|ts|tsx)', '**/*.(test|spec).(js|jsx|ts|tsx)'],

  // 忽略的文件和目录
  testPathIgnorePatterns: ['/node_modules/', '/build/', '/dist/'],

  // 覆盖率配置
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/serviceWorker.js',
  ],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },

  // 模块路径映射
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@components/(.*)$': '<rootDir>/src/components/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',
  },

  // 设置文件
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],

  // 转换配置
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
    '^.+\\.css$': 'jest-transform-css',
  },

  // 模块文件扩展名
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],

  // 清除 mock
  clearMocks: true,

  // 详细输出
  verbose: true,

  // 监听模式下的通知
  notify: true,
  notifyMode: 'failure-change',
}

// setupTests.js - 测试设置文件
import '@testing-library/jest-dom'

// 全局 mock
global.fetch = require('jest-fetch-mock')

// 自定义匹配器
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling
    if (pass) {
      return {
        message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true,
      }
    } else {
      return {
        message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false,
      }
    }
  },
})

// 全局测试钩子
beforeAll(() => {
  console.log('开始测试套件')
})

afterAll(() => {
  console.log('测试套件完成')
})

基础测试编写

单元测试

// utils/math.js - 被测试的工具函数
export const add = (a, b) => a + b

export const subtract = (a, b) => a - b

export const multiply = (a, b) => a * b

export const divide = (a, b) => {
  if (b === 0) {
    throw new Error('除数不能为零')
  }
  return a / b
}

export const factorial = (n) => {
  if (n < 0) {
    throw new Error('负数没有阶乘')
  }
  if (n === 0 || n === 1) {
    return 1
  }
  return n * factorial(n - 1)
}

export const isPrime = (num) => {
  if (num < 2) return false
  for (let i = 2; i <= Math.sqrt(num); i++) {
    if (num % i === 0) return false
  }
  return true
}

// utils/__tests__/math.test.js - 单元测试
import { add, subtract, multiply, divide, factorial, isPrime } from '../math'

describe('数学工具函数', () => {
  // 基础测试
  describe('add', () => {
    test('应该正确相加两个正数', () => {
      expect(add(2, 3)).toBe(5)
    })

    test('应该正确处理负数', () => {
      expect(add(-1, 1)).toBe(0)
      expect(add(-2, -3)).toBe(-5)
    })

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

    test('应该正确处理零', () => {
      expect(add(0, 5)).toBe(5)
      expect(add(5, 0)).toBe(5)
    })
  })

  // 异常测试
  describe('divide', () => {
    test('应该正确相除两个数', () => {
      expect(divide(10, 2)).toBe(5)
      expect(divide(7, 2)).toBe(3.5)
    })

    test('除数为零时应该抛出错误', () => {
      expect(() => divide(10, 0)).toThrow('除数不能为零')
      expect(() => divide(10, 0)).toThrow(Error)
    })
  })

  // 递归函数测试
  describe('factorial', () => {
    test('应该正确计算阶乘', () => {
      expect(factorial(0)).toBe(1)
      expect(factorial(1)).toBe(1)
      expect(factorial(5)).toBe(120)
    })

    test('负数应该抛出错误', () => {
      expect(() => factorial(-1)).toThrow('负数没有阶乘')
    })
  })

  // 参数化测试
  describe('isPrime', () => {
    test.each([
      [2, true],
      [3, true],
      [5, true],
      [7, true],
      [11, true],
      [13, true],
    ])('isPrime(%i) 应该返回 %s', (num, expected) => {
      expect(isPrime(num)).toBe(expected)
    })

    test.each([
      [1, false],
      [4, false],
      [6, false],
      [8, false],
      [9, false],
      [10, false],
    ])('isPrime(%i) 应该返回 %s', (num, expected) => {
      expect(isPrime(num)).toBe(expected)
    })
  })
})

异步测试

// services/api.js - API 服务
export class ApiService {
  constructor(baseURL) {
    this.baseURL = baseURL
  }

  async fetchUser(id) {
    const response = await fetch(`${this.baseURL}/users/${id}`)
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    return response.json()
  }

  async createUser(userData) {
    const response = await fetch(`${this.baseURL}/users`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    })

    if (!response.ok) {
      throw new Error(`创建用户失败: ${response.status}`)
    }

    return response.json()
  }

  async updateUser(id, userData) {
    const response = await fetch(`${this.baseURL}/users/${id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    })

    return response.json()
  }

  // 返回 Promise 的方法
  getUserWithTimeout(id, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error('请求超时'))
      }, timeout)

      this.fetchUser(id)
        .then((user) => {
          clearTimeout(timer)
          resolve(user)
        })
        .catch((error) => {
          clearTimeout(timer)
          reject(error)
        })
    })
  }
}

// services/__tests__/api.test.js - 异步测试
import { ApiService } from '../api'

// Mock fetch
global.fetch = jest.fn()

describe('ApiService', () => {
  let apiService

  beforeEach(() => {
    apiService = new ApiService('https://api.example.com')
    fetch.mockClear()
  })

  // Promise 测试
  describe('fetchUser', () => {
    test('应该成功获取用户数据', async () => {
      const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' }

      fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockUser,
      })

      const user = await apiService.fetchUser(1)

      expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1')
      expect(user).toEqual(mockUser)
    })

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

      await expect(apiService.fetchUser(999)).rejects.toThrow('HTTP error! status: 404')
    })

    test('应该处理网络错误', async () => {
      fetch.mockRejectedValueOnce(new Error('网络错误'))

      await expect(apiService.fetchUser(1)).rejects.toThrow('网络错误')
    })
  })

  // 使用 resolves/rejects 匹配器
  describe('createUser', () => {
    test('应该成功创建用户', () => {
      const newUser = { name: 'Jane Doe', email: 'jane@example.com' }
      const createdUser = { id: 2, ...newUser }

      fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => createdUser,
      })

      return expect(apiService.createUser(newUser)).resolves.toEqual(createdUser)
    })

    test('创建失败时应该抛出错误', () => {
      fetch.mockResolvedValueOnce({
        ok: false,
        status: 400,
      })

      return expect(apiService.createUser({})).rejects.toThrow('创建用户失败: 400')
    })
  })

  // 超时测试
  describe('getUserWithTimeout', () => {
    test('应该在超时前返回用户数据', async () => {
      const mockUser = { id: 1, name: 'John Doe' }

      fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockUser,
      })

      const user = await apiService.getUserWithTimeout(1, 1000)
      expect(user).toEqual(mockUser)
    })

    test('应该在超时后抛出错误', async () => {
      // 模拟慢响应
      fetch.mockImplementationOnce(() => new Promise((resolve) => setTimeout(resolve, 2000)))

      await expect(apiService.getUserWithTimeout(1, 500)).rejects.toThrow('请求超时')
    }, 1000) // 设置测试超时时间
  })
})

Mock 和 Spy

函数 Mock

// utils/logger.js - 日志工具
export class Logger {
  constructor(level = 'info') {
    this.level = level
  }

  log(message, level = 'info') {
    if (this.shouldLog(level)) {
      console.log(`[${level.toUpperCase()}] ${new Date().toISOString()}: ${message}`)
    }
  }

  error(message) {
    this.log(message, 'error')
  }

  warn(message) {
    this.log(message, 'warn')
  }

  info(message) {
    this.log(message, 'info')
  }

  debug(message) {
    this.log(message, 'debug')
  }

  shouldLog(level) {
    const levels = ['debug', 'info', 'warn', 'error']
    return levels.indexOf(level) >= levels.indexOf(this.level)
  }
}

// services/userService.js - 用户服务
import { Logger } from '../utils/logger'
import { ApiService } from './api'

export class UserService {
  constructor(apiService, logger) {
    this.apiService = apiService
    this.logger = logger || new Logger()
  }

  async getUser(id) {
    try {
      this.logger.info(`获取用户 ${id}`)
      const user = await this.apiService.fetchUser(id)
      this.logger.info(`成功获取用户 ${user.name}`)
      return user
    } catch (error) {
      this.logger.error(`获取用户失败: ${error.message}`)
      throw error
    }
  }

  async createUser(userData) {
    try {
      this.logger.info('创建新用户')
      const user = await this.apiService.createUser(userData)
      this.logger.info(`用户创建成功: ${user.id}`)
      return user
    } catch (error) {
      this.logger.error(`用户创建失败: ${error.message}`)
      throw error
    }
  }
}

// services/__tests__/userService.test.js - Mock 测试
import { UserService } from '../userService'
import { Logger } from '../../utils/logger'
import { ApiService } from '../api'

// Mock 整个模块
jest.mock('../api')
jest.mock('../../utils/logger')

describe('UserService', () => {
  let userService
  let mockApiService
  let mockLogger

  beforeEach(() => {
    // 创建 mock 实例
    mockApiService = new ApiService()
    mockLogger = new Logger()

    userService = new UserService(mockApiService, mockLogger)
  })

  describe('getUser', () => {
    test('应该成功获取用户并记录日志', async () => {
      const mockUser = { id: 1, name: 'John Doe' }
      mockApiService.fetchUser.mockResolvedValue(mockUser)

      const result = await userService.getUser(1)

      expect(mockApiService.fetchUser).toHaveBeenCalledWith(1)
      expect(mockLogger.info).toHaveBeenCalledWith('获取用户 1')
      expect(mockLogger.info).toHaveBeenCalledWith('成功获取用户 John Doe')
      expect(result).toEqual(mockUser)
    })

    test('应该处理错误并记录错误日志', async () => {
      const error = new Error('用户不存在')
      mockApiService.fetchUser.mockRejectedValue(error)

      await expect(userService.getUser(999)).rejects.toThrow('用户不存在')

      expect(mockLogger.info).toHaveBeenCalledWith('获取用户 999')
      expect(mockLogger.error).toHaveBeenCalledWith('获取用户失败: 用户不存在')
    })
  })

  describe('createUser', () => {
    test('应该成功创建用户', async () => {
      const userData = { name: 'Jane Doe', email: 'jane@example.com' }
      const createdUser = { id: 2, ...userData }

      mockApiService.createUser.mockResolvedValue(createdUser)

      const result = await userService.createUser(userData)

      expect(mockApiService.createUser).toHaveBeenCalledWith(userData)
      expect(mockLogger.info).toHaveBeenCalledWith('创建新用户')
      expect(mockLogger.info).toHaveBeenCalledWith('用户创建成功: 2')
      expect(result).toEqual(createdUser)
    })
  })
})

// 手动 Mock 示例
describe('UserService with manual mocks', () => {
  test('应该使用手动创建的 mock', async () => {
    // 手动创建 mock 函数
    const mockFetchUser = jest.fn()
    const mockLogger = {
      info: jest.fn(),
      error: jest.fn(),
    }

    const mockApiService = {
      fetchUser: mockFetchUser,
    }

    const userService = new UserService(mockApiService, mockLogger)
    const mockUser = { id: 1, name: 'John Doe' }

    mockFetchUser.mockResolvedValue(mockUser)

    const result = await userService.getUser(1)

    expect(mockFetchUser).toHaveBeenCalledWith(1)
    expect(mockLogger.info).toHaveBeenCalledTimes(2)
    expect(result).toEqual(mockUser)
  })
})

Spy 监听

// utils/cache.js - 缓存工具
export class Cache {
  constructor() {
    this.data = new Map()
    this.hits = 0
    this.misses = 0
  }

  get(key) {
    if (this.data.has(key)) {
      this.hits++
      return this.data.get(key)
    } else {
      this.misses++
      return null
    }
  }

  set(key, value, ttl = 0) {
    this.data.set(key, {
      value,
      expires: ttl > 0 ? Date.now() + ttl : 0,
    })
  }

  delete(key) {
    return this.data.delete(key)
  }

  clear() {
    this.data.clear()
    this.hits = 0
    this.misses = 0
  }

  getStats() {
    return {
      hits: this.hits,
      misses: this.misses,
      hitRate: this.hits / (this.hits + this.misses) || 0,
    }
  }

  cleanup() {
    const now = Date.now()
    for (const [key, item] of this.data.entries()) {
      if (item.expires > 0 && item.expires < now) {
        this.data.delete(key)
      }
    }
  }
}

// utils/__tests__/cache.test.js - Spy 测试
import { Cache } from '../cache'

describe('Cache', () => {
  let cache

  beforeEach(() => {
    cache = new Cache()
  })

  describe('基础功能', () => {
    test('应该能够设置和获取值', () => {
      cache.set('key1', 'value1')
      expect(cache.get('key1')).toBe('value1')
    })

    test('应该正确统计命中和未命中', () => {
      cache.set('key1', 'value1')

      cache.get('key1') // 命中
      cache.get('key2') // 未命中
      cache.get('key1') // 命中

      const stats = cache.getStats()
      expect(stats.hits).toBe(2)
      expect(stats.misses).toBe(1)
      expect(stats.hitRate).toBeCloseTo(0.67, 2)
    })
  })

  describe('Spy 测试', () => {
    test('应该监听方法调用', () => {
      // 监听 get 方法
      const getSpy = jest.spyOn(cache, 'get')

      cache.set('key1', 'value1')
      cache.get('key1')
      cache.get('key2')

      expect(getSpy).toHaveBeenCalledTimes(2)
      expect(getSpy).toHaveBeenCalledWith('key1')
      expect(getSpy).toHaveBeenCalledWith('key2')
      expect(getSpy).toHaveBeenNthCalledWith(1, 'key1')
      expect(getSpy).toHaveBeenNthCalledWith(2, 'key2')

      // 恢复原始实现
      getSpy.mockRestore()
    })

    test('应该能够 mock 方法返回值', () => {
      const getSpy = jest.spyOn(cache, 'get')
      getSpy.mockReturnValue('mocked value')

      const result = cache.get('any-key')
      expect(result).toBe('mocked value')

      getSpy.mockRestore()
    })

    test('应该能够 mock 方法实现', () => {
      const cleanupSpy = jest.spyOn(cache, 'cleanup')
      const mockImplementation = jest.fn(() => {
        console.log('自定义清理逻辑')
      })

      cleanupSpy.mockImplementation(mockImplementation)

      cache.cleanup()

      expect(mockImplementation).toHaveBeenCalled()

      cleanupSpy.mockRestore()
    })
  })

  describe('定时器测试', () => {
    beforeEach(() => {
      jest.useFakeTimers()
    })

    afterEach(() => {
      jest.useRealTimers()
    })

    test('应该正确处理 TTL', () => {
      cache.set('key1', 'value1', 1000) // 1秒 TTL

      expect(cache.get('key1')).toBe('value1')

      // 快进时间
      jest.advanceTimersByTime(500)
      expect(cache.get('key1')).toBe('value1') // 还未过期

      jest.advanceTimersByTime(600)
      cache.cleanup()
      expect(cache.get('key1')).toBeNull() // 已过期
    })
  })
})

快照测试

组件快照测试

// components/UserCard.jsx - React 组件
import React from 'react'

export const UserCard = ({ user, onEdit, onDelete, showActions = true }) => {
  if (!user) {
    return <div className="user-card empty">用户信息不存在</div>
  }

  return (
    <div className="user-card">
      <div className="user-avatar">
        <img src={user.avatar || '/default-avatar.png'} alt={user.name} />
      </div>
      <div className="user-info">
        <h3 className="user-name">{user.name}</h3>
        <p className="user-email">{user.email}</p>
        {user.role && <span className="user-role">{user.role}</span>}
        {user.isActive && <span className="user-status active">活跃</span>}
      </div>
      {showActions && (
        <div className="user-actions">
          <button onClick={() => onEdit(user.id)} className="btn-edit">
            编辑
          </button>
          <button onClick={() => onDelete(user.id)} className="btn-delete">
            删除
          </button>
        </div>
      )}
    </div>
  )
}

// components/__tests__/UserCard.test.jsx - 快照测试
import React from 'react'
import { render } from '@testing-library/react'
import { UserCard } from '../UserCard'

describe('UserCard', () => {
  const mockUser = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    avatar: 'https://example.com/avatar.jpg',
    role: 'admin',
    isActive: true,
  }

  const mockHandlers = {
    onEdit: jest.fn(),
    onDelete: jest.fn(),
  }

  test('应该渲染完整的用户卡片', () => {
    const { container } = render(<UserCard user={mockUser} {...mockHandlers} />)

    expect(container.firstChild).toMatchSnapshot()
  })

  test('应该渲染没有操作按钮的用户卡片', () => {
    const { container } = render(<UserCard user={mockUser} showActions={false} {...mockHandlers} />)

    expect(container.firstChild).toMatchSnapshot()
  })

  test('应该渲染空状态', () => {
    const { container } = render(<UserCard user={null} {...mockHandlers} />)

    expect(container.firstChild).toMatchSnapshot()
  })

  test('应该渲染最小用户信息', () => {
    const minimalUser = {
      id: 2,
      name: 'Jane Doe',
      email: 'jane@example.com',
    }

    const { container } = render(<UserCard user={minimalUser} {...mockHandlers} />)

    expect(container.firstChild).toMatchSnapshot()
  })
})

// 内联快照测试
describe('UserCard inline snapshots', () => {
  test('应该生成内联快照', () => {
    const user = { id: 1, name: 'Test User', email: 'test@example.com' }
    const { getByText } = render(<UserCard user={user} />)

    expect(getByText('Test User')).toMatchInlineSnapshot(`
      <h3
        class="user-name"
      >
        Test User
      </h3>
    `)
  })
})

数据快照测试

// utils/dataProcessor.js - 数据处理工具
export class DataProcessor {
  static processUserData(rawData) {
    return rawData.map((user) => ({
      id: user.id,
      name: user.full_name || `${user.first_name} ${user.last_name}`,
      email: user.email_address || user.email,
      avatar: user.profile_picture || user.avatar_url,
      isActive: user.status === 'active',
      lastLogin: user.last_login_at ? new Date(user.last_login_at) : null,
      permissions: user.roles?.map((role) => role.permissions).flat() || [],
    }))
  }

  static generateReport(users) {
    const activeUsers = users.filter((user) => user.isActive)
    const inactiveUsers = users.filter((user) => !user.isActive)

    return {
      timestamp: new Date().toISOString(),
      summary: {
        total: users.length,
        active: activeUsers.length,
        inactive: inactiveUsers.length,
        activeRate: ((activeUsers.length / users.length) * 100).toFixed(2) + '%',
      },
      topDomains: this.getTopEmailDomains(users),
      recentLogins: this.getRecentLogins(users, 7),
    }
  }

  static getTopEmailDomains(users) {
    const domains = {}
    users.forEach((user) => {
      const domain = user.email.split('@')[1]
      domains[domain] = (domains[domain] || 0) + 1
    })

    return Object.entries(domains)
      .sort(([, a], [, b]) => b - a)
      .slice(0, 5)
      .map(([domain, count]) => ({ domain, count }))
  }

  static getRecentLogins(users, days) {
    const cutoff = new Date()
    cutoff.setDate(cutoff.getDate() - days)

    return users
      .filter((user) => user.lastLogin && user.lastLogin > cutoff)
      .sort((a, b) => b.lastLogin - a.lastLogin)
      .slice(0, 10)
      .map((user) => ({
        id: user.id,
        name: user.name,
        lastLogin: user.lastLogin.toISOString(),
      }))
  }
}

// utils/__tests__/dataProcessor.test.js - 数据快照测试
import { DataProcessor } from '../dataProcessor'

describe('DataProcessor', () => {
  // Mock Date 以确保快照稳定
  beforeAll(() => {
    jest.useFakeTimers()
    jest.setSystemTime(new Date('2024-04-10T10:00:00Z'))
  })

  afterAll(() => {
    jest.useRealTimers()
  })

  describe('processUserData', () => {
    test('应该正确处理用户数据', () => {
      const rawData = [
        {
          id: 1,
          first_name: 'John',
          last_name: 'Doe',
          email_address: 'john@example.com',
          profile_picture: 'https://example.com/john.jpg',
          status: 'active',
          last_login_at: '2024-04-09T15:30:00Z',
          roles: [{ permissions: ['read', 'write'] }, { permissions: ['admin'] }],
        },
        {
          id: 2,
          full_name: 'Jane Smith',
          email: 'jane@company.com',
          status: 'inactive',
          last_login_at: null,
          roles: [],
        },
      ]

      const processed = DataProcessor.processUserData(rawData)
      expect(processed).toMatchSnapshot()
    })

    test('应该处理空数据', () => {
      const processed = DataProcessor.processUserData([])
      expect(processed).toMatchSnapshot()
    })
  })

  describe('generateReport', () => {
    test('应该生成完整的用户报告', () => {
      const users = [
        {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com',
          isActive: true,
          lastLogin: new Date('2024-04-08T10:00:00Z'),
        },
        {
          id: 2,
          name: 'Jane Smith',
          email: 'jane@example.com',
          isActive: true,
          lastLogin: new Date('2024-04-09T14:30:00Z'),
        },
        {
          id: 3,
          name: 'Bob Johnson',
          email: 'bob@company.com',
          isActive: false,
          lastLogin: new Date('2024-03-15T09:00:00Z'),
        },
      ]

      const report = DataProcessor.generateReport(users)
      expect(report).toMatchSnapshot()
    })
  })

  // 属性匹配器用于动态值
  describe('generateReport with property matchers', () => {
    test('应该生成报告(忽略时间戳)', () => {
      const users = [
        {
          id: 1,
          name: 'Test User',
          email: 'test@example.com',
          isActive: true,
          lastLogin: new Date(),
        },
      ]

      const report = DataProcessor.generateReport(users)

      expect(report).toMatchSnapshot({
        timestamp: expect.any(String),
        recentLogins: expect.arrayContaining([
          expect.objectContaining({
            lastLogin: expect.any(String),
          }),
        ]),
      })
    })
  })
})

测试覆盖率和报告

覆盖率配置

// jest.config.js - 覆盖率详细配置
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/serviceWorker.js',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}'
  ],

  coverageDirectory: 'coverage',

  coverageReporters: [
    'text',           // 控制台输出
    'text-summary',   // 简要摘要
    'lcov',          // lcov.info 文件
    'html',          // HTML 报告
    'json',          // JSON 报告
    'cobertura'      // Cobertura XML
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    // 特定文件的阈值
    './src/utils/': {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90
    },
    './src/components/': {
      branches: 75,
      functions: 75,
      lines: 75,
      statements: 75
    }
  },

  // 覆盖率路径映射
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/coverage/',
    '/build/',
    '/dist/',
    '/.storybook/',
    '/stories/'
  ]
}

// package.json - 测试脚本
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:coverage:watch": "jest --coverage --watchAll",
    "test:ci": "jest --coverage --watchAll=false --passWithNoTests",
    "test:update-snapshots": "jest --updateSnapshot"
  }
}

自定义报告器

// reporters/customReporter.js - 自定义报告器
class CustomReporter {
  constructor(globalConfig, options) {
    this.globalConfig = globalConfig
    this.options = options
  }

  onRunStart(results, options) {
    console.log('🚀 开始运行测试...')
  }

  onTestStart(test) {
    console.log(`📝 开始测试: ${test.path}`)
  }

  onTestResult(test, testResult, aggregatedResult) {
    const { numFailingTests, numPassingTests, numPendingTests } = testResult

    if (numFailingTests > 0) {
      console.log(`${test.path}: ${numFailingTests} 失败, ${numPassingTests} 通过`)
    } else {
      console.log(`${test.path}: ${numPassingTests} 通过`)
    }
  }

  onRunComplete(contexts, results) {
    const { numFailedTests, numPassedTests, numTotalTests } = results

    console.log('\n📊 测试结果汇总:')
    console.log(`总计: ${numTotalTests}`)
    console.log(`通过: ${numPassedTests}`)
    console.log(`失败: ${numFailedTests}`)

    if (results.coverageMap) {
      const coverage = results.coverageMap.getCoverageSummary()
      console.log('\n📈 覆盖率:')
      console.log(`行覆盖率: ${coverage.lines.pct}%`)
      console.log(`函数覆盖率: ${coverage.functions.pct}%`)
      console.log(`分支覆盖率: ${coverage.branches.pct}%`)
      console.log(`语句覆盖率: ${coverage.statements.pct}%`)
    }
  }
}

module.exports = CustomReporter

// 在 jest.config.js 中使用
module.exports = {
  reporters: ['default', ['./reporters/customReporter.js', { option1: 'value1' }]],
}

总结

Jest 为 JavaScript 测试提供了完整的解决方案:

  1. 零配置启动:开箱即用的测试体验
  2. 丰富的匹配器:全面的断言支持
  3. 强大的 Mock 系统:灵活的模拟和监听
  4. 快照测试:UI 和数据的回归测试
  5. 覆盖率报告:详细的测试覆盖率分析

掌握 Jest,你就能构建出可靠、可维护的测试体系!


Jest 是现代 JavaScript 测试的标准工具,值得深入学习和实践。