测试的道理

前端之家收集整理的这篇文章主要介绍了测试的道理前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

  本文转载自一位资深软件开发工程师(曾在美国Google工作,现不详)撰写的对软件测试的理解,文章的观点很独特,和软件测试业界非常普遍的看法还是有很大的不同,非常值得软件开发和测试人员阅读,并且根据自己的实际工作经历,进行相应的思考和评论
  
  原文出自以下链接
http://www.jianshu.com/p/d6ff2e433590

  原文内容我没修改,只是做了一些必要的章节区分,以便于更直观地看出文章的脉络。以下是原文内容

1. 前言:

  在长期的程序语言研究和实际工作中,我摸索出了一些关于测试的道理。然而在我工作过的每一个公司,我发现绝大多数人都不明白这些道理,很多团队集体性的采用错误的做法而不自知。很多人把测试当成一种主义和教条,进行过度的测试,不必要的测试,不可靠的测试,并且把这些错误的做法传授给新手,造成恶性循环。本来目的是提高代码质量,结果不但没能达到目的,反而降低了代码质量,增大了工作量,大幅度延缓工程进度。

  我也写测试,但我的测试方式比“测试教条主义者”们的方式聪明很多。在我心目中,代码本身的地位大大的高于测试。我不忽视测试,但我不会本末倒置,过分强调测试,我并不推崇测试驱动开发(TDD)。我知道该测试什么,不该测试什么,什么时候该写测试,什么时候不该写,什么时候应该推迟测试,什么时候完全不需要测试。因为这个原因,再加上高强的编程能力,我多次完成别人认为在短时间不可能完成的任务,并且制造出质量非常高的代码

2. 测试的道理

  现在我就把这些自己领悟到的关于测试的道理总结一下,其中有一些是鲜为人知或者被误解的。

2.1 不要以为你处处显示出“重视代码质量”的态度,就能提高代码质量。

  总有些人,以为自己知道“单元测试”(unit test),“集成测试”(integration test)这样的名词,就很懂编程,就可以教育其他人。可惜,光有态度和口号是不解决问题的,你还必须有实战的技巧,深入的见解和智慧,必须切实地知道应该怎么做。
  代码的质量不会因为你重视它就得到提升,也不会因为你采取了措施(比如测试,静态分析)就一定会得到改善。你必须知道什么时候该写测试,什么时候不该写测试,需要写测试的时候,要写什么样的测试。
  其实,提高代码质量唯一可行的手段不是写测试,而是反复的提炼自己的思维,写简单清晰的代码。如果你想真的提高代码质量,我的文章『编程的智慧』是一个不错的出发点。

(《编程的智慧》链接http://www.jianshu.com/p/7645a5ea7f46

2.2 真正的编程高手不会被测试捆住手脚。

  是的,你身边那个你认为“不很在乎测试”的家伙,也许是个比你更好的程序员。我喜欢把编程比喻成开赛车,而测试就是放在路边用来防撞的轮胎护栏……

  护栏有时候是很有用,可以救命的,然而一个合格的车手,绝对不会一心想着有护栏保护,测试在编程活动中的地位也应该就是这样。优秀的车手会很快看见优雅而简单的路径,恰到好处地掌握速度和时机,直奔终点而去。护栏只是放在最危险的地段,让你出了意外不要死得太惨。护栏并不能让你成为好的车手,不能让你取得冠军。绝大多数时候,你的安全只有靠自己的技术,而不是护栏,你永远有办法可以撞死自己。测试的作用也是一样,即使有了很多的测试,代码的安全仍然只掌握在你的手里。你永远可以制造出新的 bug,而没有测试可以检测到它……

  通常情况下,一个合格的车手是根本碰不到这些护栏的,他们心里想的是更高的目标:快点到达终点。相比之下,一个不合格的车手,他经常撞到赛道外面去,所以在他的心里,护栏有着至高无上的地位,所以他总是跟别人宣扬护栏的重要性。他开车的时候为了防止犯错,要在他经过的路径两边密密麻麻摆上护栏,甚至把护栏摆到赛道中间,以确保自己的转弯幅度正确。他在护栏之间跌跌撞撞,最后只能算是勉强到达终点。鼓吹测试驱动开发的人,就是这种三流车手,这种人写再多的测试也不可能倒腾出可靠的代码来。

2.3 在程序和算法定型之前,不要写测试。

  TDD (Test Drive Develop,测试驱动开发)的教条者喜欢跟你说,在写程序之前就应该先写测试。为什么写代码之前要写测试呢?这只是一种教条。这些人其实没有用自己的脑子思考过这个问题,而只是人云亦云,觉得这样“很酷”,符合潮流,或者以为这样做了别人就会认为自己是高手。实际上在程序框架完成,算法定型之前,你都不需要写测试。如果你想知道代码是否正确,用人工方式运行代码,看看结果足以。

  如果你发现编程初期需要保证的性质纷繁复杂,如此之多,不写测试你就没信心的话,那你还是想办法先提高下基本的编程技术吧:多做练习,简化代码,让代码更加模块化,看看我的『编程的智慧』或者『SICP』一类的东西。写测试并不能提高你的水平,正好相反,过早的写测试会捆住你的手脚,让你无法自由的修改代码和算法。如果你不能很快的修改代码,不能用直觉感觉到它的变化和结构,而是因为测试而处处卡顿,你的头脑里就不能产生所谓“flow)”,就不能写出优雅的代码来,结果到最后你什么也没学会。只有在程序不再需要大幅度的改动之后,才是逐渐加入测试的时候。

2.4 不要为了写测试而改变本来清晰的编程方式。

  很多人为了满足“覆盖”(coverage)的要求,为了可以测试到某些模块,或者为了使用 mock,而把本来简单清晰地代码改成更加复杂而混淆的形式,甚至采用大量 reflection。这样一来其实降低了代码的质量。本来很简单的代码,一眼看去就知道是否正确,可是现在你一眼看过去,到处都是为了方便测试而加进去的各种转接插头,再也无法感觉到代码。这些用来辅助测试的代码,阻碍了你对代码进行直觉思维,而如果你不能把代码的逻辑完全映射在头脑里(进而产生直觉),你是很难写出真正可靠的代码的。

  有些 C# 程序员,为了测试而加入大量的 interface 和 reflection,因为这样可以在测试的时候很方便的把一片代码替换成 mock。结果你就发现这程序里每个类都有一个配套的 interface,还需要写另外一个 mock 类,去实现这个 interface。这样一来,不但代码变得复杂难以理解,而且还损失了 Visual Studio 的协助功能:你不再能按一个键(F12)就直接跳转方法的定义,而需要先跳到对应的 interface 方法,然后再找到正确的实现。所以你不再能够在代码里面快速跳转浏览。这种方便性的损失,会大幅度降低头脑产生整体理解的机会。而且为了 mock,每一个构造函数调用都得换成一个含有 reflection 的构造,使得编译器的静态类型检查无法确保类型正确,增加运行时出错的可能性,出错信息还难以理解,得不偿失的后果。

2.5 不要测试“实现细节”,因为那等同于把代码写两遍。

  测试应该只描述程序需要满足的“基本性质”(比如 sqrt(4) 应该等于 2),而不是去描述“实现细节”(比如具体的开平方算法的步骤)。有些人的测试过于详细,甚至把代码的每个实现步骤都兢兢业业的进行测试:第一步必须做A,第二步必须做B,第三步必须做C…… 还有些人喜欢给 UI 写测试,他们的测试里经常这样写:如果你浏览到这个页面,那么你应该在标题栏看见这行字……

  仔细想一下就会发现,这种作法本质上不过是把代码(或者UI)写了两遍而已。本来代码里面明白写着:先做A,再做B,再做C。UI 描述文件里面明白写着:标题栏里面是这些内容。你有什么必要在测试里把它们全都再检查一遍呢?这根本没有增加任何可靠性:你在代码里会犯错,你把同样的逻辑换种形式再写一遍,难道就不会错了吗?

  这就像某些脑子秀逗的人,他出门时总是担心门没锁好,关门之后要推推拉拉好几次,确认门是锁上了的。还没走几步,他仍然在怀疑门没锁好,又走回去推推拉拉好几次,却始终不能放心 :P 这种做法非但不能保证代码的正确,反而给修改代码制造了障碍。理所当然,你把同一段代码写了两遍,每当要修改代码,你就得修改两次!这样的测试就像紧箍咒一样,把代码压得密不透风。每一次修改代码,都会导致很多测试失败,以至于这些测试都不得不重写。本质上就是把代码修改了两遍,只不过更加痛苦一些。

2.6 并不是每修复一个 bug 都需要写测试。

  很多公司都流传一个常见的教条,就是认为每修复一个 bug,都需要为它写测试,用于确保这个 bug 不再发生。甚至有人要求你这样修复一个 bug:先写一个测试,重现这个 bug,然后修复它,确保测试通过。这种思维其实是一种生搬硬套的教条主义,它会严重的减慢工程的进度,而代码的质量却不会得到提高。写测试之前,你应该仔细的思考一个问题:这个 bug 有多大可能会在同一个地方再次发生?很多低级错误一旦被看出来之后,它就不大可能在同一个地方再次出现。在这种情况下,你只需手工验证一下 bug 消失了就可以。

  为不可能再出现的 bug 大费周折,写 reproducer,构造各种数据结构去验证它,保证它下次不会再出现,其实是多此一举。同样的低级错误就算再出现,也很可能不在同一个地方。写测试不但不能保证它不再发生,而且浪费你很多时间。这测试在每次 build 的时候都会消耗时间,每次编译都因为这些测试多花几分钟,累积起来之后,你就发现工程进度明显减慢。只有当发现已有的测试没有抓住程序必须满足的重要性质时,你才应该写新的测试。你不应该是为这个 bug 而写测试,而是为代码的性质而写测试。这个测试的内容不应该只是防止这个 bug 再次发生,而是要确保 bug 所反映出来的,之前缺失的“性质”得到保证。

2.7 避免使用 mock,特别是多层的 mock。

  很多人写测试都喜欢用很多 mock,堆积很多层,以为只有这样才能测试到路径比较深的模块。其实这样不但非常繁琐费事,而且多层的 mock 往往不能产生足够多样化的输入,不能覆盖各种边界情况。如果你发现测试需要进行多层的 mock,那你应该考虑一下,也许你需要的不是 mock,而是改写代码,让它更加模块化。如果你的代码足够模块化,你不应该需要多层的 mock 来测试它。你只需要为每一个模块准备一些输入(包括边界情况),确保它们的输出符合要求。然后你把这些模块像管道一样连接起来,形成一个更大的模块,测试它也符合输入输出要求,以此类推。

2. 8 不要过分重视“测试自动化”,人工测试也是测试。

  写测试,这个词往往隐含了“自动运行”的含义,也就是假设了要不经人工操作,完全自动的测试。打一个命令,它过一会就会告诉你哪些地方有问题。然而,人们往往忽略了“人工测试”。他们没有意识到,人工去试验,去观察,也是一种测试。所以你就发现这样的情况,由于自动测试在很多时候非常难以构造(比如,如果你要测试一段复杂的交互式GUI代码的响应),很多人花了很多时间,利用各种测试框架和工具,甚至遥控 WEB 浏览器去做一些自动操作,花太多时间却发现各种不可靠,没法测到很多东西。

  其实换一个思路,他们只需要花几分钟的时间,就可以用人工的方式观察到很多深入的问题。过分的重视测试自动化的原因,往往在于一个不切实际的假设,他们假设错误会频繁的再次发生,所以自动化了可以省下人的力气。但是其实,一旦一个 bug 被修好,它反复出现的机会不会很大的。过分的要求测试自动化,不但延缓了工程进度,让程序员恼火,效率低下,而且失去了人工测试的精确性。

2.9 避免写太长,太耗时的测试。

  很多人写测试,叽里呱啦很长一串,到后来再看的时候,他已经不记得自己当时想测什么了。有些人本来用很小的输入就可以测试到需要的性质,他却总喜欢给一个很大的输入,下意识的以为这样更加靠谱,结果这测试每次都会消耗大量的 build 时间,而其实达到的效果跟很小的输入没有任何区别。

2.10 一个测试只测试一个方面,避免重复测试。

  有些人一个测试测很多内容,结果每次那个测试失败,都搞不清楚到底是哪个部件出了问题。有些人为了“放心”,喜欢在多个测试里面“附带”测某些他认为相关的部件,结果每次那个部件出问题,就发现好多个测试失败。如果一个测试只测一个方面,不重复测同一个部件,那么你就可以很快的根据失败的测试,发现出问题的部件和位置。

2.11 避免通过比较字符串来进行测试。

  很多人写测试的时候,喜欢通过打印出一些东西,然后使用字符串比较的方式来决定输出是否符合要求。一个常见的做法是把输出打印成格式化的 JSON,然后对比两个文本。甚至有人 JSON 都不用,直接就比较 printf 输出的结果。这种测试是非常脆弱的。因为字符串输出的格式往往会发生微小的变化,比如有人在里面加了一个空格之类的。把这种字符串作为标准输出,进行字符串比较,很容易因为微小的改动而使大量测试失败,导致很多的测试需要做不必要的修改。正确的做法,应该是进行结构化的比较,如果你要把标准结果存成 JSON,那么你应该先 parse 出 JSON 所表示的对象,然后再进行结构化的对比。PySonar2 的测试就是这样的做法,所以相当的稳定。

2.12 “测试能帮助后来人”的误区。

  每当指出测试教条主义的错误,就会有人出来说:“测试不是为了你自己,而是为了你走了以后,以后进来的人不犯错误。” 首先,这种人根本没有看清楚我在说什么,因为我从来没有反对过合理的测试。其次,这种“测试能帮助后来人”,其实是没有经过实践检验,站不住脚的说法。如果你的代码写得很乱,就算你测试再多,后来人也无法理解,反倒被莫名其妙的测试失败给弄得更糊涂,不知道是自己错了还是测试错了。我已经说过了,测试不能完全保证代码不被改错,实际上它们防止代码被改错的作用是非常弱的。无论如何,后来人都必须理解原来的代码的逻辑,知道它在做什么,否则他们不可能做出正确的修改,就算你有再严密的测试也一样。

  举一个亲身的例子。我在 Google 做出 PySonar 之后,最后一个测试都没写。第二次我回到 Google,我的上司 Steve Yegge 对我说:“你走了之后,我改了一些你的代码,真是太清晰,太好把握了,修改你的代码是一种快乐!” 这说明什么问题呢?我并不是说你可以不写测试,但这个例子说明,测试对于后来人的作用,并不是你有些人想象的那么大。创造清晰的代码才是解决这个问题的关键。

  这种怕人突然走了,代码无法维护的想法,导致了一些人对测试过分的重视,但测试却不能解决这种问题。相反,如果测试太繁琐,做不必要的测试,反而容易让员工不满,容易走人,去加入在这方面更加有见地的公司。有些公司以为有了测试,就可以随便打发人走,这种想法是大错特错的。你需要明白的一个事情是,代码永远是属于写出它的那个人的,就算有测试也一样。如果核心人物真的走了,就算你有再多的测试也没用的,所以解决方法就是把他们留住!一个有远见的公司总是通过其他的手段解决这个问题,比如优待和尊重员工,创造良好的氛围,使得他们没那么快想走。另外,公司必须注意知识的传承,防止某些代码只有一个人理解。

……

总结:

  由于绝大多数人对测试的误解如此之深,测试教条主义的流毒如此之广,导致许许多多优秀的程序员沉沦在繁琐的测试驱动开发中,无法舒展自己的长处。为了大家有一个轻松,顺利又可靠的工作环境,我希望大家多多转发这篇文章,改变这个行业的陋习。我希望大家在工程中理性的对待测试,而不是盲目的写测试,只有这样才能更好更快的完成项目。

猜你在找的设计模式相关文章