Jest 最佳实践 #

测试原则 #

FIRST 原则 #

text
┌─────────────────────────────────────────────────────────────┐
│                    FIRST 原则                               │
├─────────────────────────────────────────────────────────────┤
│  F - Fast(快速)     测试应该快速执行                        │
│  I - Independent(独立)测试之间不应有依赖                    │
│  R - Repeatable(可重复)任何环境都能得到相同结果             │
│  S - Self-validating(自验证)测试结果应该是明确的            │
│  T - Timely(及时)测试应该与代码同步编写                     │
└─────────────────────────────────────────────────────────────┘

测试金字塔 #

text
          /\
         /  \
        / E2E\        - 少量端到端测试
       /------\
      / 集成测试 \      - 适量集成测试
     /----------\
    /   单元测试   \    - 大量单元测试
   /--------------\

测试组织 #

文件结构 #

text
src/
├── components/
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.styles.js
│   │   ├── Button.test.jsx
│   │   └── index.js
│   └── Input/
│       ├── Input.jsx
│       └── Input.test.jsx
├── utils/
│   ├── math.js
│   └── math.test.js
└── __tests__/
    ├── integration/
    │   └── user-flow.test.js
    └── e2e/
        └── checkout.test.js

测试文件命名 #

javascript
// 单元测试
math.test.js
math.spec.js

// 集成测试
api.integration.test.js
user-flow.integration.test.js

// E2E 测试
checkout.e2e.test.js

测试结构 #

javascript
describe('Component/Module name', () => {
  describe('method/feature name', () => {
    describe('scenario', () => {
      test('should expected behavior', () => {
        // Arrange
        // Act
        // Assert
      });
    });
  });
});

命名规范 #

测试描述 #

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

// ✅ 好的命名
test('should return sum of two numbers', () => {});
test('should throw error when input is invalid', () => {});
test('should render button with correct text', () => {});

使用一致的命名模式 #

javascript
// 模式:should [expected behavior] when [condition]
test('should return true when user is logged in', () => {});
test('should return false when password is incorrect', () => {});
test('should throw error when email is invalid', () => {});

测试策略 #

单元测试 #

javascript
// 测试纯函数
describe('formatDate', () => {
  test('formats date correctly', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toBe('2024-01-15');
  });

  test('handles invalid input', () => {
    expect(() => formatDate(null)).toThrow();
  });
});

组件测试 #

javascript
// 测试组件行为
describe('Button', () => {
  test('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button')).toHaveTextContent('Click me');
  });

  test('handles click event', async () => {
    const onClick = jest.fn();
    render(<Button onClick={onClick}>Click me</Button>);
    
    await userEvent.click(screen.getByRole('button'));
    
    expect(onClick).toHaveBeenCalled();
  });
});

API 测试 #

javascript
// 测试 API 端点
describe('POST /api/users', () => {
  test('creates user with valid data', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'John', email: 'john@example.com' });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      name: 'John',
      email: 'john@example.com',
    });
  });
});

测试隔离 #

避免共享状态 #

javascript
// ❌ 不好的做法
describe('shared state', () => {
  let counter = 0;

  test('test 1', () => {
    counter++;
    expect(counter).toBe(1);
  });

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

// ✅ 好的做法
describe('isolated state', () => {
  let counter;

  beforeEach(() => {
    counter = 0;
  });

  test('test 1', () => {
    counter++;
    expect(counter).toBe(1);
  });

  test('test 2', () => {
    expect(counter).toBe(0);
  });
});

清理 Mock #

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

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

Mock 最佳实践 #

只 Mock 必要的依赖 #

javascript
// ❌ 不好的做法 - Mock 所有依赖
jest.mock('lodash');
jest.mock('axios');
jest.mock('moment');

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

使用简单的 Mock 实现 #

javascript
// ❌ 不好的做法
jest.mock('./api', () => ({
  fetch: jest.fn(() => {
    return new Promise((resolve) => {
      setTimeout(() => resolve({ data: 'test' }), 100);
    });
  }),
}));

// ✅ 好的做法
jest.mock('./api', () => ({
  fetch: jest.fn().mockResolvedValue({ data: 'test' }),
}));

断言最佳实践 #

使用语义化断言 #

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

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

一个测试一个断言点 #

javascript
// ❌ 不好的做法
test('user operations', () => {
  const user = createUser();
  expect(user.id).toBeDefined();
  expect(user.name).toBe('John');
  expect(user.email).toBe('john@example.com');
});

// ✅ 好的做法
test('creates user with id', () => {
  const user = createUser();
  expect(user.id).toBeDefined();
});

test('creates user with correct name', () => {
  const user = createUser();
  expect(user.name).toBe('John');
});

异步测试 #

正确处理异步 #

javascript
// ❌ 不好的做法
test('bad async', () => {
  fetchData().then(data => {
    expect(data).toBe('expected');
  });
});

// ✅ 好的做法
test('good async', async () => {
  const data = await fetchData();
  expect(data).toBe('expected');
});

设置合理的超时 #

javascript
test('slow operation', async () => {
  const result = await slowOperation();
  expect(result).toBeDefined();
}, 10000); // 10 秒超时

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
        with:
          files: ./coverage/lcov.info

package.json scripts #

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

测试覆盖率 #

设置合理的阈值 #

javascript
// jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

关键代码更高覆盖率 #

javascript
coverageThreshold: {
  './src/core/': {
    branches: 90,
    functions: 90,
    lines: 90,
  },
  './src/utils/': {
    branches: 80,
    functions: 80,
    lines: 80,
  },
},

性能优化 #

并行执行 #

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

增量测试 #

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

# 只运行与 git 变更相关的测试
jest --changedSince=main

缓存 #

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

测试文档 #

使用注释说明复杂测试 #

javascript
test('handles complex discount calculation', () => {
  // 场景:用户购买 3 件商品,其中 1 件打折
  // 预期:总价 = 原价 * 2 + 折扣价 * 1
  const items = [
    { price: 100, discount: 0 },
    { price: 100, discount: 0 },
    { price: 100, discount: 0.2 },
  ];

  const total = calculateTotal(items);

  expect(total).toBe(280);
});

常见反模式 #

1. 测试实现细节 #

javascript
// ❌ 不好的做法
expect(wrapper.vm.count).toBe(1);

// ✅ 好的做法
expect(wrapper.text()).toContain('1');

2. 过度 Mock #

javascript
// ❌ 不好的做法
jest.mock('lodash');
jest.mock('./utils');

// ✅ 好的做法
jest.mock('axios');

3. 测试依赖顺序 #

javascript
// ❌ 不好的做法
test('test 1', () => {
  sharedState = 'value';
});

test('test 2', () => {
  expect(sharedState).toBe('value');
});

// ✅ 好的做法
beforeEach(() => {
  sharedState = 'value';
});

最佳实践清单 #

text
□ 测试原则
  ├── 遵循 FIRST 原则
  ├── 遵循测试金字塔
  └── 测试用户行为

□ 测试组织
  ├── 合理的文件结构
  ├── 一致的命名规范
  └── 清晰的测试结构

□ 测试隔离
  ├── 避免共享状态
  ├── 清理 Mock
  └── 独立的测试

□ Mock 使用
  ├── 只 Mock 必要的依赖
  ├── 使用简单的 Mock 实现
  └── 正确清理 Mock

□ 断言
  ├── 使用语义化断言
  ├── 一个测试一个断言点
  └── 清晰的错误消息

□ 异步测试
  ├── 正确处理异步
  ├── 设置合理的超时
  └── 使用 async/await

□ CI/CD
  ├── 自动化测试
  ├── 覆盖率报告
  └── 测试分片

□ 性能
  ├── 并行执行
  ├── 增量测试
  └── 缓存优化

总结 #

编写好的测试需要:

  1. 理解测试目的 - 测试是为了保证代码质量,不是为了覆盖率
  2. 关注用户行为 - 测试用户看到和交互的内容
  3. 保持简单 - 简单的测试更容易维护
  4. 持续改进 - 定期回顾和优化测试代码

好的测试是项目成功的关键,投入时间学习和实践测试技术是值得的!

最后更新:2026-03-28