Skip to content

Latest commit

 

History

History
179 lines (137 loc) · 13.6 KB

File metadata and controls

179 lines (137 loc) · 13.6 KB

如何写好单元测试

1.为什么要测单元测试
2.单元测试与集成测试的区别
3.先写代码还是先写单元测试
4.谁来编写什么样的测试
5.如何避免无用的测试
  5.1.只写必要的测试
  5.2.只写关键的测试
  5.3.无用的测试
6.测试代码覆盖率
7.单元测试中的"伪装术"(mock仿件‘桩件或者我们说的的打桩)

1.为什么要测单元测试

1.提升软件质量、减少Bug、提升反馈速度、减少重复工作、提高开发效率.
2.让开发者对程序稳定性更有信心,让我们尽早发现Bug.
3.单元测试是保证我们在进行重构时,不会影响到代码的外部接口功能,让我们的重构是十分安全的.


单元测重要性:
  1.编写执行单元测试,就是为了证明这段代码的行为和我们期望的一致.

  2.单元测试增加代码量,单元测试代码是系统代码的两倍或更多,但是同时节省了调试程序及减少了Bug相应的也就节约了修复Bug的时间.
  
  3.如果没有写单元测试,没有发现bug的情况下.在测试人员测试的时候才发现问题,或者在线上用户使用的时候才发现问题.在去修复bug,
  测试也会花大量的时间去做了重复的测试.在某些场景下bug导致大量的数据丢失,要花很大精力去修复丢失的数据.

2.单元测试与集成测试的区别

1.测试粒度不同
  单元测试的颗粒度是在单只程序上,集成测试的颗粒度则在整个系统上.

2.单元测试的目的
  对已实现的软件最小测试单元,保证软件系统中各个单元的质量,单元测试强调被测试对象的独立性即隔离性(单元2个字).

3.集成测试的目的
  也叫组装测试或者联合测试.在单元测试的基础上,将所有模块按照设计要求组装成为子系统或者系统,进行集成测试.实际中一些模块虽然能够单独地工作,
  但并不能保证组装起来也能正常的工作.程序的某些局部反应不出来的问题,在全局上很有可能暴露出来.

4.单元测试和集成测试容易混为一谈
  因为单元测试和集成测试可以使用同样的工具和框架来编写,如果不了解单元测试和集成测试的区别很容易混为一谈.

3.先写代码还是先写单元测试

TDD的全称(Test Driver Development)测试驱动开发.开发要以测试为驱动。 编码之前,测试先行.很多没有写过单元测试的开发者会想代码都没有,我连要测的对象都没有啊, 怎么写单元测试?(我们可以通过先写伪代码,或者建模来解决这个问题,这样我们站在用户的角度去开发, 尽早发现遇到的问题,也不会遗漏功能)

测试驱动开发(TDD)好处:
  1.你会站在用户的角度去看你将要完成的产品,你要尽可能想到用户所有进行的操作.而不是从程序员的角度想用户应该会如何去使用我们的产品
  
  2.单元测试用例是在对功能进行测试,在写代码之前先写测试用例,可以对我们编写代码提供指导性的参考,防止我们漏掉一些功能,或者开发的功能和实际需求不一致.
  
  3.它让开发者对自己代码有了信心,因为事先写好的单元测试用例都通过了.

  4.在更改代码后,开发者跑单元测试用例没有通过,开发者能精确的定位Bug,并轻而易举的解决Bug.

  5.一套完备的测试用例帮助开发者(把关),开发者就可以十分安全的使用(重构).重构改变的是代码的内部结构,而不会改变外部接口功能.
  测试用例是保证我们在进行重构时, 不会影响到代码的外部接口功能. 让开发者进行的重构是十分安全的.

  6.安全的重构开发者,在必要时候你还可以痛痛快快的对代码做一场大的变革.让代码变得干净了,扩展性、可以维护性以及易理解性.


测试驱动开发(TDD)概述
  1.TDD有这么多好处,它需要开发者有写出完善的测试用例的能力(这项能力是长期理论与实践的结合体),否则你将会吃了亏编写了一大堆测试用例,
  却没有用到点子上.可怕的是,你还对你“测试通过”的糟糕的代码满怀信心. 也可能你创造无尽的条件中,会觉得测试用例很难写.
  
  2.TDD的主旨是高效,可能这个高效不是非常高的开发速度
  
  3.TDD的思想是以测试推动开发进程. 因为我们在软件开发之前,每个程序单元的功能都已经确定了.开发者在理解完整个程序需求以后如果直接进行开发,
  可能会因为考虑不很周全,似乎功能实现的没有问题,但是其中却可能隐藏着非常可怕的Bug. TDD促使开发者先根据程序单元的功能编写测试代码,
  就像是先建一个模型, 然后向里面浇注合适功能的代码. 最后满足所有的测试验证并且正常通过测试,这个程序单元才算完成.
  
  4.消除了开发人员主观性的对程序单元健壮性的评估,更客观的验证每一个程序单元的功能实现以及可能出现的Bug.
  当然这些操作都需要有大量的代码支持,所以费事是在所难免的.但是这点"费事"与健壮性非常强的代码相比,大多数人还是偏向于使用TDD。

4.谁来编写什么样的测试

单元测试只能是程序员自己的责任吧? 话是没错,但我们编写单元测试的时候习惯性的往理想状况下编写.
开发者最好不要针对自己实现的代码来编写单元测试. 应该由其他开发者编写单元测试,这样即能减少bug也能提高开发者的水平.

5.如何避免无用的测试

5.1.只写必要的测试

编写自己觉得"没谱"的代码:比如说业务逻辑很复杂自己没完全吃透;或者以前没写过所以不知道是不是能行,也无法确定过程中是否会产生难以预料的变数等等.

5.2.只写关键的测试

有时候必要的测试你写不出来,又没有人指导,只能勉强可以跳过.但是关键性测试不要省.所谓关键性的测试:就是你所写代码里的核心逻辑;再换句话说就是如果一切顺利,它至少能够做到(或者不要去做)的那件事.
这就意味着你可能忽略了一些边界条件的处理, 而且你也不知道该怎么处理,但是你至少保证了最重要的那条路线是可以走通的.将来重构的时候,这条关键路线能确保你不至于茫然无措.

如果在构造关键性测试用例的时候你发现你很难触碰到那一点(比如说前置条件你不会在测试用例里处理),那么很大的可能是你的这个单元过于复杂了,
这是一个极好的立时重构信号.你可以尝试把要触碰的那一点逻辑抽取出来单独测试. 这样一来你至少做到了把核心逻辑分离出来,
其他的代码就算再糟糕重构起来也会轻松得多.

5.3.无用的测试

1.不要去测试语言的核心库和/或标准库函数
  如果你的代码简单到就调用了一句标准库函数,那还浪费什么时间去编写测试啊.这些代码都是久经考验的.虽然也有语言本身错误的小概率事件发生,
  但由于标准函数的处理过程你触碰不到(常常深埋于虚拟机中或调用系统底层接口)所以你的测试对你自己的代码丝毫没有帮助.
  (当然,如果您是专家级程序员则不在此例,说不准您就是等着解决这个问题呢)

2.不要去测试框架的基础类或工具方法
  道理和第一条类似,知名的框架都有很完善的自身测试,否则你也不敢用不是? 如果你确信是框架自身出了问题,你的测试更应该去应用在框架本身上,
  说不定你可以做出个补丁为该项目做出贡献.
  
  顺手举个例子: 你继承了某框架的(Model)层,然后在里面定义了检查其实例的某一属性是否为空的验证(使用框架自带的验证方法,而不是自己编写的).
  这种情况就没有必要测试这个检查是否生效,除非你这个类在初始化的时候返回的是其他类的实例……你项目组里有这么无聊的人吗?

3.不要.去测试外部依赖的有效性
  这是初学者常常陷的坑,而且往往把自己折磨到不行. 这里有两个问题: 第一, 如果你的测试一定需要外部依赖, 你首先应该考虑伪造它,
  而不是在 A 的测试里先检查 B(也就是说, 你的测试目标是 A, 为了完成这个测试用例, 你需要用到 B 并且 B 的某种特性一定要成立,
  这是先决条件, 于是你不得不写一句断言先测试这件事情,然后才能测试真正的目标 A).如果你能伪造一个 B, 叫 B', 那么 B' 不一定非要和 B 完全一样,
  只要它能表现出来恰好满足本次测试用例的特征就足够了.这样事情就会变得单纯的多. 其次, 即使你无法伪造 B(基本上是因为不会),
  那么你至少应该把对 B 的特性测试转移到 B 自己的单元测试中去.

4.最后还有一种测试是"无用"的
  就是从来只见它(通过)没见过它(失败)的测试.你自己都没意识到这种测试可能从头到尾都没有测试任何代码!这也是 TDD 强调先红后绿再重构的原因之一,
  你至少应该在最开始让测试用例失败一次, 否则等测试数量变多以后再去分辨就来不及了.另外重构完了也最好手动破坏一下代码(比如随便往里面打几个无意义的字符)诱使测试报错, 以确保测试真的覆盖到了目标代码.

6.测试代码覆盖率

忽视代码代码覆盖率

1.简单普及下,代码覆盖算法有很多种,大致上对比准确性:路径覆盖 > 条件覆盖 ~= 判定覆盖 > 语句覆盖.而且这只是说条件分支,循环什么的还有别的算法就不多说了.
这些算法在覆盖率都达到 100% 的前提下,其“靠谱"程度可能有天壤之别.问题就出在下决策使用代码覆盖率做考核的人往往不明白这种差别,这就给了落地执行的人可趁之机,
很容易就演变成了“在追求 100% 代码覆盖率的道路上,我们应该无所不用其极“.若是连落地执行人都不懂,那就更悲剧了,一群人对着水份极大的 100% 乐得嘴都合不拢,想想都难受.
  
2.所以对于代码覆盖率的不当应用,只会让大家越走越偏,浪费时间不说收效还甚微;反过来恰当的使用代码覆盖率又对团队的要求极高,只有一个人懂行是不够的,
因为你没有那么多时间精力去检查结果是不是真的靠谱.如果每一个人都按照靠谱的方式去写代码和测试,不用测试覆盖率也没什么大不了的.
因此如果我是初创团队的负责人,我宁可选择把时间和精力放在测试用例本身上,测试本身靠谱了,测试覆盖率的辅助价值才能靠谱.
  
3.无视它.有人认为代码覆盖率是最形式主义的技术工具,覆盖率再高也不能保证代码本身无懈可击,该出 Bug 的地方 100% 的覆盖率也救不了你.
其实作为一种辅助度量工具,代码覆盖本身并没有什么错,有位仁兄说得好:"在追求精益求精的道路上,我们应该无所不用其极".错就错在拿代码覆盖率当考核指标,
以此来衡量测试人员的工作水平,对此我相当无语,也相当反感. 有识之士一定会说:你也不要以偏概全,路径覆盖所度量出的代码覆盖率还是相当靠谱的嘛.

7.单元测试中的"伪装术"(mock仿件‘桩件或者我们说的的打桩)

伪装术(mock仿件,stubs桩件)描述:
  有时候对被测系统(mock仿件、stubs桩件)进行测试是很困难的,因为它依赖于其他无法在测试环境中使用的对象、组件、或者api.这有可能是因为这些
  (对象、组件、api)不可用,它们不会返回测试所需要的结果,或者执行它们会有不良副作用.在其他情况下,我们的测试策略要求对被测系统的内部行为
  有更多控制或更多可见性. 如果在编写测试时无法使用(或选择不使用)实际的依赖组件,可以用测试替身来代替.测试替身不需要和真正的依赖组件有完全一样的的行为方式;
  他只需要提供和真正的组件同样的 API 即可,这样被测系统就会以为它是真正的组件!
  
什么情况下使用(mock仿件、stubs桩件、):
  1.如果外部依赖不存在,则测试肯定无法通过
  2.如果外部依赖不会返回测试所需的结果,或者执行它会有不良的副作用.
  3.如果外部依赖变更,则会导致测试失败.严格来说这种后果不是测试的责任,外部依赖的变更应该保持外部接口不变和返回结果不变,只变更内部的行为.
  使用伪装术的好处就在于一旦出现这种情况不至于让你误以为是己方的代码出了问题.当然你也会想,如果用了伪装对象,那么外部依赖变了己方的测试还浑然不知,
  这不是很危险吗?有道理,不过单元测试的职责是测试己方代码的正确性,对于外部依赖的模拟不一定非得和模拟对象完全一致,真实的交互应该先由集成测试来捕捉问题,
  否则很容易迷失在复杂的代码交互之中.
  
例如:
  1.我在编写单元测试Cart购物车类,依赖Product产品类和User用户类
  2.依赖Product产品类和User用户类已经测试过了
  3.或者依赖Product产品类和User用户类是由其他人写的
  
问题:
  1.Product产品类和User用户类一旦出现情况不至于让你误以为是Cart类的代码出了问题.
  2.不用为了创造很多前置条件,才能做出断言.(如果这样应该放在集成测试)
  3.在测试购物车时,我们应该避免使用"new Cart($userUid, $productId, $quantity)"这种方式,
  如果程序中都是id去查询,这样会影响执行效率,和不利于打桩,
  我们应该使用这种"new Cart(User $user, Product $product, $quantity)"方式.