定义:同样的代码、同样的测试,不做任何修改,运行多次结果不一致——有时通过有时失败。
就像一个摇摆不定(flaky)的开关,你没碰它,它自己时开时关。
区别于真正的 bug(每次都失败)和真正通过的测试(每次都通过),flaky test 处于一种"薛定谔"状态。
多个测试文件并行执行,共享同一个全局资源(MSW server、数据库连接、全局变量等)。执行顺序不确定,A 文件的 cleanup 可能在 B 文件请求的中途发生。
// vitest.setup.js 中的 MSW 生命周期
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers()); // ← 并发时可能影响其他文件
afterAll(() => server.close());
测试假设某个异步操作会在固定时间内完成,但实际耗时受 CPU 负载、IO 等影响。
// ❌ 脆弱:CI 机器慢一点就挂
await sleep(100);
expect(element).toBeVisible();
// ✅ 稳健:轮询等待,直到条件满足或超时
await waitFor(() => expect(element).toBeVisible());
测试之间通过全局变量、localStorage、单例对象等产生耦合。A 测试修改了全局状态但没清理,B 测试在某些执行顺序下看到了脏数据。
// ❌ 测试间共享状态
let counter = 0;
test('increment', () => { counter++; expect(counter).toBe(1); });
test('check initial', () => { expect(counter).toBe(0); }); // ❌ 顺序依赖
// ✅ 每个测试独立初始化
beforeEach(() => { counter = 0; });
测试依赖了网络请求、真实数据库、系统时间、文件系统等不可控因素。
| 外部依赖 | 不稳定原因 | 解决方案 |
|---|---|---|
| 真实 API | 网络抖动、服务宕机 | MSW / nock mock |
| 数据库 | 残留数据、连接池耗尽 | 事务回滚 / 内存数据库 |
| 系统时间 | Date.now() 不确定 | vi.useFakeTimers() |
| 随机数 | Math.random() 不确定 | 固定 seed / mock |
board.integration.test.tsx 和 stores.integration.test.tsx 间歇性失败,但单独运行总是通过。
server.resetHandlers() 影响了其他文件的请求匹配
fetch('/api/employees') 未被 MSW 拦截,直接发到 localhost:3000
api.ts 尝试 fetch('/api/auth/employee/refresh'),相对 URL 在 jsdom 中报 ERR_INVALID_URL
handleAuthFailure 尝试 window.location.href = ...,jsdom 报 Not implemented: navigation
findByPlaceholderText(/search/) 超时 → FAIL
// 日志中出现了真实 Express 响应头 —— 说明 MSW 没拦截住
{
'x-powered-by': 'Express',
'x-trace-id': '1770417141324-48dbb4' // 真实后端生成的 trace ID
}
// Token refresh 用相对 URL,jsdom 无法解析
TypeError: Failed to parse URL from /api/auth/employee/refresh
cause: TypeError: Invalid URL
| 方案 | 做法 | 优缺点 |
|---|---|---|
| 快速修复 | vitest.integration.config.js 加 fileParallelism: false,串行执行 |
✅ 1 分钟搞定 ❌ 测试速度变慢(8s → ~25s) |
| 根治修复 | 确保 api.ts 的 baseURL 在测试环境下是完整 URL(http://localhost:3000),让 MSW 能正确匹配 |
✅ 保持并行速度 ❌ 需改动生产代码 |
# 连续跑 5 次,看是否一致
for i in 1 2 3 4 5; do
npx vitest run --config vitest.integration.config.js 2>&1 | grep 'Tests ';
done
# 单独跑可疑文件
npx vitest run --config vitest.integration.config.js src/__tests__/pages/appointments/board.integration.test.tsx
# 如果单独通过但全量失败 → 并发竞态
# 如果单独也偶尔失败 → 异步时序或状态问题
// 方案1:串行化(简单但慢)
// vitest.integration.config.js
export default defineConfig({
test: {
fileParallelism: false, // 所有文件串行执行
}
});
// 方案2:隔离 MSW 实例(每个文件独立 server)
// 在每个测试文件中:
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterAll(() => server.close());
// ❌ 硬编码等待时间
await new Promise(r => setTimeout(r, 500));
expect(screen.getByText('Done')).toBeInTheDocument();
// ✅ 条件等待
await waitFor(() => {
expect(screen.getByText('Done')).toBeInTheDocument();
}, { timeout: 5000 });
// ✅ 每个测试前重置所有共享状态
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
// 重置所有单例/全局变量
});
// ✅ 测试后清理副作用
afterEach(() => {
cleanup(); // React Testing Library
server.resetHandlers(); // MSW
});
waitFor / findBy 而非 sleep / getBy 处理异步retry: 2),但同时记录 flaky 次数skip + 建 ticket,不让它污染信号创建时间:2026-02-06 | 背景:项目集成测试套件 33 个文件并行执行时发现 flaky test