name: fixing-flaky-tests description: 诊断和修复在独立运行时通过但在并发运行时失败的测试。覆盖共享状态隔离和资源冲突。参考条件等待技能来处理定时问题。
修复不稳定测试
目标症状: 测试在单独运行时通过,但与其他测试一起运行时失败。
先诊断
测试单独通过,与其他测试一起失败?
│
├─ 每次都出现相同错误 → 共享状态
│ └─ 数据库、全局变量、文件、单例
│
├─ 随机/定时失败 → 竞态条件
│ └─ 使用`条件等待`技能
│
└─ 资源错误(端口、文件锁) → 资源冲突
└─ 每个测试/工作线程需要唯一资源
快速诊断:
- 单独运行失败测试10次 - 是否总是通过?
- 与套件一起运行失败测试10次 - 相同错误还是不同?
- 检查错误消息 - 是否提到端口/文件/连接?
共享状态(确定性失败)
测试污染了其他测试依赖的状态。通过每个测试隔离状态来修复。
| 状态类型 | 隔离模式 |
|---|---|
| 数据库 | 事务回滚、保存点、工作线程特定数据库 |
| 全局变量 | 在beforeEach/afterEach中重置 |
| 单例 | 每个测试提供新实例 |
| 模块状态 | jest.resetModules()或等效方法 |
| 文件 | 每个测试使用唯一路径、临时目录 |
| 环境变量 | 在设置/清理中保存/恢复 |
数据库隔离(最常见):
# Python:保存点回滚 - 每个测试回滚
@pytest.fixture
async def db_session(db_engine):
async with db_engine.connect() as conn:
await conn.begin()
await conn.begin_nested() # 保存点
# ... 生成会话 ...
await conn.rollback() # 所有更改消失
// Jest:在测试之间重置模拟
beforeEach(() => {
jest.clearAllMocks()
jest.resetModules() // 在测试前清除模块缓存
})
afterEach(() => {
jest.restoreAllMocks() // 恢复被监视的函数
})
查看语言特定参考以获取完整模式。
竞态条件(随机失败)
测试不等待异步操作完成。
使用条件等待技能 获取详细模式,包括:
- 框架特定等待(Testing Library
findBy、Playwright自动等待) - 自定义轮询助手
- 何时可接受任意超时
快速总结: 等待条件,而不是时间:
// 不好
await sleep(500)
// 好
await waitFor(() => expect(result).toBe('done'))
资源冲突(端口/文件错误)
多个测试或工作线程竞争相同资源。
工作线程特定资源:
# Python pytest-xdist:每个工作线程唯一数据库
@pytest.fixture(scope="session")
def database_url(worker_id):
if worker_id == "master":
return "postgresql://localhost/test"
return f"postgresql://localhost/test_{worker_id}"
// Jest/Node:动态端口分配
const server = app.listen(0) // 操作系统分配可用端口
const port = server.address().port
文件冲突:
import tempfile
@pytest.fixture
def temp_dir():
with tempfile.TemporaryDirectory() as d:
yield d
语言特定隔离模式
| 技术栈 | 参考 |
|---|---|
| Python (pytest, SQLAlchemy) | references/python.md |
| Jest / Testing Library | references/jest.md |
| Playwright E2E | references/playwright.md |
验证
修复后,验证修复是否有效:
# 多次运行特定测试
pytest tests/test_flaky.py -x --count=20
# 并行运行
pytest -n auto
# Jest等效
jest --runInBand # 首先验证串行工作
jest # 然后验证并行工作