- 不要测试“实现细节”,因为那等同于把代码写两遍。测试应该只描述程序需要满足的“基本性质”(比如 sqrt(4) 应该等于 2),而不是去描述“实现细节”(比如具体的开平方算法的步骤)。有些人的测试过于详细,甚至把代码的每个实现步骤都兢兢业业的进行测试:第一步必须做A,第二步必须做B,第三步必须做C…… 还有些人喜欢给 UI 写测试,他们的测试里经常这样写:如果你浏览到这个页面,那么你应该在标题栏看见这行字……
仔细想一下就会发现,这种作法本质上不过是把代码(或者UI)写了两遍而已。本来代码里面明白写着:先做A,再做B,再做C。UI 描述文件里面明白写着:标题栏里面是这些内容。你有什么必要在测试里把它们全都再检查一遍呢?这根本没有增加任何可靠性:你在代码里会犯错,你把同样的逻辑换种形式再写一遍,难道就不会错了吗?
这就像某些脑子秀逗的人,他出门时总是担心门没锁好,关门之后要推推拉拉好几次,确认门是锁上了的。还没走几步,他仍然在怀疑门没锁好,又走回去推推拉拉好几次,却始终不能放心 :P 这种做法非但不能保证代码的正确,反而给修改代码制造了障碍。理所当然,你把同一段代码写了两遍,每当要修改代码,你就得修改两次!这样的测试就像紧箍咒一样,把代码压得密不透风。每一次修改代码,都会导致很多测试失败,以至于这些测试都不得不重写。本质上就是把代码修改了两遍,只不过更加痛苦一些。 - 并不是每修复一个 bug 都需要写测试。很多公司都流传一个常见的教条,就是认为每修复一个 bug,都需要为它写测试,用于确保这个 bug 不再发生。甚至有人要求你这样修复一个 bug:先写一个测试,重现这个 bug,然后修复它,确保测试通过。这种思维其实是一种生搬硬套的教条主义,它会严重的减慢工程的进度,而代码的质量却不会得到提高。写测试之前,你应该仔细的思考一个问题:这个 bug 有多大可能会在同一个地方再次发生?很多低级错误一旦被看出来之后,它就不大可能在同一个地方再次出现。在这种情况下,你只需手工验证一下 bug 消失了就可以。
为不可能再出现的 bug 大费周折,写 reproducer,构造各种数据结构去验证它,保证它下次不会再出现,其实是多此一举。同样的低级错误就算再出现,也很可能不在同一个地方。写测试不但不能保证它不再发生,而且浪费你很多时间。这测试在每次 build 的时候都会消耗时间,每次编译都因为这些测试多花几分钟,累积起来之后,你就发现工程进度明显减慢。只有当发现已有的测试没有抓住程序必须满足的重要性质时,你才应该写新的测试。你不应该是为这个 bug 而写测试,而是为代码的性质而写测试。这个测试的内容不应该只是防止这个 bug 再次发生,而是要确保 bug 所反映出来的,之前缺失的“性质”得到保证。 - 避免使用 mock,特别是多层的 mock。很多人写测试都喜欢用很多 mock,堆积很多层,以为只有这样才能测试到路径比较深的模块。其实这样不但非常繁琐费事,而且多层的 mock 往往不能产生足够多样化的输入,不能覆盖各种边界情况。如果你发现测试需要进行多层的 mock,那你应该考虑一下,也许你需要的不是 mock,而是改写代码,让它更加模块化。如果你的代码足够模块化,你不应该需要多层的 mock 来测试它。你只需要为每一个模块准备一些输入(包括边界情况),确保它们的输出符合要求。然后你把这些模块像管道一样连接起来,形成一个更大的模块,测试它也符合输入输出要求,以此类推。
- 不要过分重视“测试自动化”,人工测试也是测试。写测试,这个词往往隐含了“自动运行”的含义,也就是假设了要不经人工操作,完全自动的测试。打一个命令,它过一会就会告诉你哪些地方有问题。然而,人们往往忽略了“人工测试”。他们没有意识到,人工去试验,去观察,也是一种测试。所以你就发现这样的情况,由于自动测试在很多时候非常难以构造(比如,如果你要测试一段复杂的交互式GUI代码的响应),很多人花了很多时间,利用各种测试框架和工具,甚至遥控 WEB 浏览器去做一些自动操作,花太多时间却发现各种不可靠,没法测到很多东西。