写测试的一些基本原则

测试的核心只有一个——确认你的代码运行结果和期待结果是一致的。

因此,当你写测试的时候,你脑子里只需要考虑两个事情

  • 我如何得到这段代码的运行结果
  • 我这段代码的期待结果是什么? 根据得到运行结果方式的不同,我们有很多的测试分类,比如黑盒或者白盒。不需要去对这些分类死记硬背,你脑海中其实只需要想我怎么拿到结果。

比如在Python中,你可以通过直接调用你写的函数求值。如果用这种方法,你就会发现,你写的代码好坏,很大程度上影响了你测试的难易。side effect越小的代码,越好测试。比如一个函数,它一个输入永远对应一个输出,这代码就非常好测试。这个函数是个method,它依赖instance的一些属性,就难测一些,需要建立一个instance。它依赖一些全局变量,更难测了。依赖远端的数据库之类的,那就难测炸了。一段代码,依赖的东西越多越复杂,测试就越困难。所以很多人会发现,只有自己开始写测试了,才能意识到自己的代码写的有什么毛病。

除了直接调用,也可以通过subprocess去开一个新的进程。有时候这是必须的,比如viztracer这种命令行工具,你是不能通过完全的直接调用去做测试的。那这里你就需要对怎么开一个新进程做一个了解,或许还需要自己整理一个小小的framework来测试你的工具。

得到了实际运行结果之后,就要和期待结果进行比对。你的程序一定得有个期待结果才能去测试,不然你光跑一下程序是几乎没有意义的(倒是能确认不报错)。

在设计期待结果的时候,要注意一下这个结果是不是稳定的。比如我去读我的stdout打印某一串字符串——那它未来的输出是不是可能变?我在写pdb的代码的时候,经常会涉及测试中显示代码在文件中行数的问题,这就是一个很重要的考量。你把测试写死了,后面就总得调整,改一下代码就得改测试,你的测试就失去了价值。

既然聊到了测试的价值,我们就说一下——为什么要写测试? 测试当然是确认此时此刻,你的程序是正常工作的。但是它更大的意义在于——增强了未来你对程序做修改的时候你对程序还工作的信心。

在你刚开始开发某个功能的时候,可能手动做了非常多的测试,一步一步地把它弄得正常工作。这时候你可能觉得我为什么要写那些我已经做过的测试?浪费时间啊。但这些自动化测试的意义在于,在未来你改动代码的时候,随时保证你之前的那些功能还正常工作。因为你不会在未来修改代码的时候再这么详细地测试你之前开发的功能了。

如果你的代码缺少测试,你会很快陷入一个开发瓶颈——完全不敢重构和做大型的修改,只敢做新功能。因为根本不知道改之后我这东西还工作不工作。随着你项目复杂度的增加,各个模块之间耦合度变大,任何一个部分的修改都可能影响到很多内容,你就没法接着写了,然后项目就烂尾了。

所以,如果你想做一个相对长期稳定维护的项目,测试是不可少的。具体的测试上的技术细节,其实并没有那么重要。就像我前面说的,无非就是想办法运行你的代码,再和期待值做比较。测试的代码经常没那么“漂亮”,甚至有不少冗余,这都完全是可被接受的。甚至很多时候,要适度减少测试代码中的抽象,以保证每个单独的测试的完整性(读测试代码的时候不用几个函数之间来回切)。