- 发布于
Jest 测试框架完全指南:JavaScript 单元测试与集成测试最佳实践
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
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 测试提供了完整的解决方案:
- 零配置启动:开箱即用的测试体验
- 丰富的匹配器:全面的断言支持
- 强大的 Mock 系统:灵活的模拟和监听
- 快照测试:UI 和数据的回归测试
- 覆盖率报告:详细的测试覆盖率分析
掌握 Jest,你就能构建出可靠、可维护的测试体系!
Jest 是现代 JavaScript 测试的标准工具,值得深入学习和实践。