记一个由 Surge 更新发现的 Bug
缘起
我最近在做一个 Phoenix 项目,这个项目在我加入之前就有了不错的测试覆盖率。但是在我开始不久后,其中一个测试用例在我的机器上变得会一直超时(之前则没问题),在 CI 环境和其他开发的机器上都没问题,而我也没有改过相关代码或者测试。
这就让我非常疑惑(但是兴奋?)了,立马开始了调查。
调查
经过一番调查后发现,被测试的代码会使用 curl
从一个外链尝试下载,成功后则解析返回的结果并更新数据库。
而我和同事网络环境上唯一的不同是我使用了 Surge1 作为代理, 而把 Surge 关闭之后再次尝试,这个测试就不再超时了。
最终发现引发这个问题的原因是:
- 为了不在测试环境中使用
curl
对外部环境发起请求,之前在写这个测试时将curl
用一个私有函数downloader/2
包装了一下,并当作公共函数的依赖注入进去,在测试时则注入一个构造好的failure_downloader/2
Stub 失败时的行为。 - 而同事在写 Stub 部分时忘记了将
failure_downloader/2
注入进去了,即在测试时依然会正常发起curl
请求。 - 同事又按照测试环境下
curl
对example.org
的请求返回,写了assertion
。因此这个测试在 CI 和其他同事的机器上都能正常通过。 - 而 Surge 在某一次更新后,对
example.org
的处理与正常情况下并不一致,导致了这个测试在我的机器上正常了一段时间后失败。
种种巧合导致了这个问题,我觉得也算一件趣事。
P.S. 最后,我又好奇问同事这个 Module 在线上有没有出问题,结果他说线上环境还没有用到这个 Module。(YAGNI2 的又一体现)
False Negative
其实这个问题在使用 Mock/Stub/Fake 的测试中非常常见,又被称作 False Negative Error3。即:
测试结果表示一个模块没有正常工作,而实际上这个模块的行为是正常的。
这个问题说明我们的测试代码写得不够完善。
而解决这个问题最好的办法就是: Watch the test fail 4。
每次写测试时,都确保这个测试能按照我们预想的方式失败:
- TDD 的时候,每次写完测试/代码都运行一遍,并能得到预想的失败信息。
- 即使写出了一个通过了的测试(为了增加覆盖率),也要对被测试的代码做出修改,保证这个测试覆盖的部分被注释后,这个测试能按我们预想的方式失败,并给出合适的失败信息。
关于 False Negative,我最初是在 How to stop hating your tests.5 这个 Talk 中学到的,其中还有更多写 Test 的技巧,值得一看。