跳到主要内容
我写的测试,好像全是在证明自己没错

我写的测试,好像全是在证明自己没错

今天也没搞懂
今天也没搞懂

· 阅读约 6 分钟

上个月刷到的那篇讲证伪的文章,当时直接划过去了。昨晚又翻出来,看到一点多。

不是看不懂。是看懂了,有点不想承认。

文章开头那个思想实验:一个生物学家说凡是放进冷冻柜的青蛙都会死。然后一批一批地做,十批,一百只,全死了。作者说这仍然证明不了那句话;但第十一批里只要有一只活着,那句话就倒了。

这个不对称我当然知道。Dijkstra 那句「测试只能揭示缺陷的存在,永远无法揭示缺陷的不存在」,我背得出来,还拿它当签名档用了小半个月,用得挺得意。

问题是我背得出这句话,跟我写测试时脑子里在想什么,完全是两回事。

今天下午我把那个小破记账页面的测试翻出来看了一遍。一共十一个。名字大概长这样:

def test_add_expense_returns_200():
def test_delete_removes_item():
def test_total_sums_all_categories():

一眼扫下来,全是「加进去能成」「删掉会少」「加起来对」。全是我心里早就认定它对的情况下,再让机器点一次头。

一个「如果我这个假设是错的,它会以什么方式错」都没问过。

也不能说这些测试没用……只是它们的功能好像不是找错,是让我按下运行键看到一排绿的,然后安心去泡杯茶。

这个认知有点难受。

文章里那个排序函数的例子我盯着看了挺久。作者写了个先 set() 去重再排序的函数,空列表、单元素、一堆普通列表全过。然后反例是 [0, 0],期望 [0, 0],实际出来 [0]。

就这么小。小到有点侮辱人。

我第一反应是「这我也会写错吗」,第二反应才是「等下,我是不是写过类似的东西」。

去翻了一下,没找到一模一样的。但翻的过程里,我在那个按类别汇总的函数上停了很久,因为我想不起来如果某一类下面只有一条金额为 0 的记录,我那段代码会给什么。到现在我也没去试。

然后是 Google 那一段。

他们在一个大的 MySQL 环境里,主动把几百个数据库里的一个弄挂了,就为了看会发生什么。几分钟之内一堆依赖服务报错,内外用户进不去,实验当场叫停。

我读了两遍。第二遍不是因为没看懂,是我意识到我从来没主动弄坏过自己的任何东西。

不是不想。是不敢。我那个项目里唯一一份能跑的版本,我连改个变量名都要先复制一份备份,你让我主动把数据库停掉看反应?

Netflix 那段我跳着看的。只记住一个意思:让工程师更频繁地遇到故障,他们才会把服务做得更抗。这话有点狠,我承认我现在做不到。

后面还有一大段讲支付重试的。单元测试集成测试全绿,团队默认「同一个请求重试不会重复扣款」,作者自己构造了「扣款成功但响应超时丢了、客户端重试」这个顺序,结果第二笔真的又扣了一次。

这段我停得最久。我根本没做过支付,谈不上共鸣。停在那儿是因为「所有测试都是绿的,然后假设本身就是错的」这个组合,我太熟了。前阵子那个导出功能就是这么回事,跑了一晚上没报错,第二天打开文件发现只有表头。

……

说回来。文章后面还讲了属性测试、模型检验、fuzzing。模型检验和 fuzzing 我基本跳过了,看不进去,TLA+ 那两个字一出来我脑子就自动关机。

属性测试那段我是真去试了。

它的思路听起来对我这种人有救:不是写「输入 A 应该输出 B」,是写「不管输入什么,输出都得满足某个性质」,然后让工具自己去找违反的。找反例这件事交给机器,我不用自己想。

我照着一个排序的性质,写了这么个东西:

from hypothesis import given
import hypothesis.strategies as st

@given(st.lists(st.integers()))
def test_my_sort(xs):
    out = my_sort(xs)
    assert out == sorted(out)
    assert sorted(out) == sorted(xs)

跑起来了。两秒钟,给我丢了个反例出来。

我第一反应是——「这算反例吗?」

盯着那个输入看了半天,越看越觉得是 hypothesis 自己有问题。然后去翻评论区,发现真有人专门在说这件事:反例得先是真的才算反例,青蛙活着也可能是因为冷冻柜根本没通电;测试变红也可能是 fixture 坏了。作者还回了句,说会把这一层补进文章。

行吧。

我把那段丢给同桌看,它说你这个空列表进来的时候返回的是 None。

就这么个事。Hypothesis 没错,是我错了。

写到这我有点想笑,又有点不想笑。这个错我三天前就该发现,只要我肯花两分钟想一下「输入是空的会怎么样」。我没想。我想的是「这个函数我写得挺清楚的」。

现在的问题是,我知道该往哪个方向改了,但具体怎么改还是夹生的。

比如「输出是输入的排列」这条性质,我是照着文章抄的写法,抄完其实没太想明白它为什么和「输出非递减」要分成两条,合成一条会怎么样。比如我那个函数其实能吃浮点数,但我只给了 integers,那我到底测到了什么?再比如 shrink 之后那个最小的反例,我怎么知道它已经是最小了,工具说最小就最小吗?

这些我一个都没搞明白。

凌晨一点半关编辑器之前,我在那个测试文件最上面加了一行注释:

# 这个文件里的东西,目的不是让它变绿

写完自己觉得有点中二。

但今天早上打开电脑,我第一件事是去翻那个汇总函数,看金额为 0 的时候会怎么样。结果没测出来——我造数据的时候发现页面上压根加不进去 0 块的记录,输入框先把它拦了。所以这到底算不算个问题,我还没想明白。

三周前的我看到这篇文章,大概只会点个赞收藏,然后继续写下一排「能跑就放心」的测试。这么一比,好像也不算毫无进展。

评论区蹲一个大佬,谁懂「性质该怎么写、写到多细算够」这种事,求教教我。这块我是真的夹生。