上周同事在群里发了个截图,某个开源项目提了 PR 之后 CI 挂了,测试工具报错信息是测试工具 AssertionError: expected 200 but got 503。他盯着屏幕看了十分钟,测试工具最后发现是测试工具测试环境里一个 mock 服务没起来。这种事我见过太多次了——工具本身没问题,测试工具问题出在人对工具的测试工具理解上。
测试工具大概分三类。测试工具第一类是测试工具单元测试框架,JUnit、测试工具pytest、测试工具Jest 这些,测试工具你写 assert 它们负责跑和报结果。测试工具第二类是测试工具 mock 和 stub 工具,Mockito、测试工具unittest.mock、nock,用来隔离外部依赖。第三类是 CI 里跑的那些集成测试工具,比如 Postman 的 Newman、Selenium、Playwright。很多人把这三类混着用,结果就是测试跑得慢、挂得莫名其妙。
我刚开始写测试的时候,特别喜欢把什么都塞进单元测试里。数据库查询要连真库,HTTP 请求要发真请求,美其名曰“端到端”。结果本地跑一遍两分钟,CI 上跑五分钟,挂了还得去查是网络问题还是代码问题。后来一个老同事跟我说了句话,我记到现在:“单元测试里出现网络调用,就像在卧室里开挖掘机。” 话糙理不糙。单元测试就该是毫秒级的,依赖全部 mock 掉。集成测试才去碰真实环境,但那种测试数量要少,跑得慢就慢,反正一天也就跑几次。
Mock 工具用起来也有讲究。Python 的 unittest.mock 有个 patch 装饰器,很多人一上来就 @patch('requests.get'),然后整个测试文件里所有 requests 调用都被替换了。结果另一个测试用例想测真实请求,忘了加 patch.stopall(),直接报错。我的习惯是尽量用 with patch(...) 上下文管理器,作用域清晰,跑完自动还原。Jest 的 jest.mock 也是,提升到文件顶部之后所有测试都受影响,不如在具体测试里用 jest.spyOn 加 mockRestore。
“工具不会替你思考,它只会放大你已有的习惯。” —— 某个加班到凌晨的测试工程师
Playwright 刚出来那会儿,我拿它重写了公司一个 Selenium 的登录测试。同样的用例,Selenium 要等 WebDriverWait 加 expected_conditions,代码写了三十行,跑起来还偶尔 flaky。Playwright 直接 await page.click('button'),自动等待元素可点击,八行搞定。但这不是说 Playwright 就万能了。它有次在我本地跑得好好的,CI 上一直超时,查了半天发现是 Docker 镜像里缺了中文字体,页面渲染慢了一拍。工具再好,环境不对照样翻车。
还有一个容易忽略的点:测试工具本身的配置。pytest 的 conftest.py 里写 fixture,作用域选 function 还是 session,直接影响跑测试的时间。我见过一个项目,每个测试用例都重新建一次数据库连接,两千个用例跑完要四十分钟。改成 session 作用域之后,降到三分钟。但代价是测试之间可能互相污染,得靠事务回滚或者清表来隔离。没有银弹,只有取舍。
最后说个真实的翻车案例。有个项目用 Jest 的 --coverage 跑覆盖率,阈值设了 80%,低于就挂 CI。开发为了过 CI,给一堆 getter 和 setter 写了测试,覆盖率上去了,但那些测试什么都没验证,就是 expect(obj.prop).toBeDefined()。后来重构的时候,有人把 getter 逻辑改错了,测试全绿,线上直接炸了。覆盖率是工具,不是目标。工具告诉你哪里没测,不告诉你测得好不好。
所以回到开头那个 CI 挂掉的事。如果那个项目把 mock 服务用 testcontainers 或者 docker-compose 管起来,在测试启动前确保依赖就绪,根本不会有那十分钟的排查。工具选对了,还得用对。用不对,再好的工具也就是个高级摆设。