Jest 最佳实践 #

测试原则 #

FIRST 原则 #

text
┌─────────────────────────────────────────────────────────────┐
│                    FIRST 原则                                │
├─────────────────────────────────────────────────────────────┤
│  F - Fast         测试应该快速执行                           │
│  I - Independent  测试应该相互独立                           │
│  R - Repeatable   测试应该可重复                             │
│  S - Self-validating 测试应该自我验证                        │
│  T - Timely       测试应该及时编写                           │
└─────────────────────────────────────────────────────────────┘

测试金字塔 #

text
        /\
       /  \      E2E 测试(少量)
      /────\     
     /      \    集成测试(适量)
    /────────\   
   /          \  单元测试(大量)
  /────────────\

测试组织 #

文件结构 #

text
project/
├── src/
│   ├── components/
│   │   ├── Button.jsx
│   │   └── Button.test.jsx
│   ├── utils/
│   │   ├── math.js
│   │   └── math.test.js
│   └── services/
│       ├── userService.js
│       └── userService.test.js
├── __tests__/
│   ├── integration/
│   │   └── api.test.js
│   └── e2e/
│       └── userFlow.test.js
└── __mocks__/
    └── axios.js

测试文件命名 #

text
# 单元测试
Component.test.jsx
module.test.js
service.test.ts

# 集成测试
integration.test.js
api.test.js

# E2E 测试
e2e.test.js
userFlow.test.js

测试命名规范 #

描述性命名 #

javascript
// ✅ 好的命名
describe('UserService', () => {
  describe('createUser', () => {
    test('should create user with valid data', () => {});
    test('should throw error when email is missing', () => {});
    test('should throw error when email is invalid', () => {});
  });
});

// ❌ 不好的命名
describe('test', () => {
  test('test1', () => {});
  test('test2', () => {});
});

中文命名(团队约定) #

javascript
describe('用户服务', () => {
  describe('创建用户', () => {
    test('应该成功创建有效用户', () => {});
    test('邮箱缺失时应该抛出错误', () => {});
    test('邮箱格式错误时应该抛出错误', () => {});
  });
});

测试结构 #

AAA 模式 #

javascript
test('should calculate total price', () => {
  // Arrange - 准备
  const cart = new Cart();
  const items = [
    { price: 100, quantity: 2 },
    { price: 50, quantity: 1 },
  ];
  
  // Act - 执行
  const total = cart.calculateTotal(items);
  
  // Assert - 断言
  expect(total).toBe(250);
});

Given-When-Then 模式 #

javascript
test('should apply discount to total', () => {
  // Given - 给定
  const cart = new Cart();
  cart.addItem({ price: 100 });
  
  // When - 当
  cart.applyDiscount(0.1);
  
  // Then - 那么
  expect(cart.total).toBe(90);
});

测试隔离 #

独立测试 #

javascript
// ✅ 好的做法 - 每个测试独立
describe('User', () => {
  let user;

  beforeEach(() => {
    user = new User('John');
  });

  test('should have correct name', () => {
    expect(user.name).toBe('John');
  });

  test('should update name', () => {
    user.setName('Jane');
    expect(user.name).toBe('Jane');
  });
});

// ❌ 不好的做法 - 测试依赖
describe('User', () => {
  let user = new User('John');

  test('test 1', () => {
    user.setName('Jane');
  });

  test('test 2', () => {
    // 依赖 test 1 的状态
    expect(user.name).toBe('Jane');
  });
});

清理资源 #

javascript
afterEach(() => {
  jest.clearAllMocks();
});

afterAll(() => {
  database.close();
});

Mock 最佳实践 #

Mock 边界 #

javascript
// ✅ Mock 外部依赖
jest.mock('axios');
jest.mock('./api');

// ❌ 不要 Mock 被测试的代码
jest.mock('./calculator'); // 错误!

清理 Mock #

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

使用 spyOn #

javascript
test('spy on method', () => {
  const spy = jest.spyOn(obj, 'method');
  
  obj.method('arg');
  
  expect(spy).toHaveBeenCalledWith('arg');
  
  spy.mockRestore();
});

断言最佳实践 #

使用语义化断言 #

javascript
// ✅ 好的做法
expect(user).toBeDefined();
expect(items).toHaveLength(3);
expect(error).toThrow();

// ❌ 不好的做法
expect(user !== undefined).toBe(true);
expect(items.length === 3).toBe(true);

单一断言 #

javascript
// ✅ 好的做法 - 每个测试一个断言点
test('should calculate total', () => {
  const total = calculateTotal(items);
  expect(total).toBe(150);
});

test('should apply discount', () => {
  const discounted = applyDiscount(100, 0.1);
  expect(discounted).toBe(90);
});

// ❌ 不好的做法 - 多个断言
test('cart operations', () => {
  expect(cart.items).toHaveLength(1);
  expect(cart.total).toBe(100);
  expect(cart.discount).toBe(0);
});

异步测试最佳实践 #

使用 async/await #

javascript
// ✅ 好的做法
test('fetches user', async () => {
  const user = await fetchUser(1);
  expect(user).toBeDefined();
});

// ❌ 不好的做法
test('fetches user', () => {
  fetchUser(1).then(user => {
    expect(user).toBeDefined();
  });
});

错误处理 #

javascript
// ✅ 好的做法
test('handles error', async () => {
  await expect(fetchUser(-1)).rejects.toThrow();
});

// ❌ 不好的做法
test('handles error', async () => {
  try {
    await fetchUser(-1);
  } catch (error) {
    expect(error).toBeDefined();
  }
});

测试覆盖策略 #

优先级 #

text
1. 核心业务逻辑 - 100% 覆盖
2. 工具函数 - 90%+ 覆盖
3. UI 组件 - 70%+ 覆盖
4. 配置文件 - 可选覆盖

覆盖率目标 #

javascript
// jest.config.js
coverageThreshold: {
  global: {
    branches: 70,
    functions: 70,
    lines: 70,
    statements: 70,
  },
  './src/core/': {
    branches: 90,
    functions: 90,
    lines: 90,
  },
},

CI/CD 集成 #

GitHub Actions #

yaml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - run: npm ci
      - run: npm test -- --coverage --ci
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

GitLab CI #

yaml
test:
  image: node:18
  script:
    - npm ci
    - npm test -- --coverage --ci
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'

Pre-commit Hook #

json
// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "pre-push": "npm test"
    }
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "jest --bail --findRelatedTests"
    ]
  }
}

性能优化 #

并行执行 #

javascript
// jest.config.js
module.exports = {
  maxWorkers: '50%',
};

缓存 #

javascript
module.exports = {
  cache: true,
  cacheDirectory: '/tmp/jest_cache',
};

增量测试 #

bash
# 只运行修改的测试
jest --onlyChanged

# 基于分支变更
jest --changedSince=main

常见陷阱 #

1. 测试实现细节 #

javascript
// ❌ 不好的做法
test('internal state', () => {
  expect(component.state('count')).toBe(0);
});

// ✅ 好的做法
test('user behavior', () => {
  expect(screen.getByText('0')).toBeInTheDocument();
});

2. 过度 Mock #

javascript
// ❌ 不好的做法 - Mock 一切
jest.mock('./utils');
jest.mock('./services');
jest.mock('./components');

// ✅ 好的做法 - 只 Mock 外部依赖
jest.mock('axios');

3. 忽略边界情况 #

javascript
// ✅ 测试边界情况
test.each([
  [0, 0],
  [1, 1],
  [-1, -1],
  [100, 100],
])('handles %i', (input, expected) => {
  expect(process(input)).toBe(expected);
});

test('handles null', () => {
  expect(() => process(null)).toThrow();
});

test('handles undefined', () => {
  expect(() => process(undefined)).toThrow();
});

测试文档 #

测试描述文档化 #

javascript
/**
 * UserService 测试套件
 * 
 * 测试范围:
 * - 用户创建
 * - 用户查询
 * - 用户更新
 * - 用户删除
 * 
 * 边界情况:
 * - 无效输入
 * - 重复邮箱
 * - 权限检查
 */
describe('UserService', () => {
  // 测试代码
});

总结 #

测试检查清单 #

text
□ 测试命名清晰描述行为
□ 测试相互独立
□ 使用 AAA 模式组织测试
□ Mock 外部依赖
□ 测试边界情况
□ 测试错误处理
□ 保持测试简洁
□ 定期运行测试
□ CI 中强制运行
□ 保持覆盖率目标

恭喜你完成了 Jest 测试框架的学习!现在你已经掌握了从入门到专家的完整知识体系,可以开始在实际项目中应用这些技能了!

最后更新:2026-03-28