目录

 

​一.TDD和BDD​

​1.TDD 测试驱动开发(Test-Driven Development)​

​2.BDD    行为驱动开发(Behavior Driven Development)​

​二.怎样的测试才算优秀​

​三.测试替身​

​         1.为什么要mock?​

         ​​2、测试替身的类型​

​         3.使用测试替身的指南   ​

​         4.准备、执行、断言​

​         5.什么是可测试的业务代码规则​

​         6、可测的设计的指南​


 

一.TDD和BDD

1.TDD 测试驱动开发(Test-Driven Development)

        (1) TDD在敏捷开发模式中被称之为“测试先行的编程(test-first programming)

               使用自动化测试作为设计工具将世界颠倒过来了,当你用测试设计代码时,

设计,编码,测试”序列变成“测试,编码,设计”。测试先于编码,并以追溯性的设计活动来得出结论。那结论性的设计活动成为重构,序列变成“测试,编码,重构”。

              

单元测试学习笔记(一)_模块化

         (2) TDD,基于一个简单的想法:“在编写出能够证明代码存在的失败测试之前,不写生产代码”。这也是它有时被称为测试先行编程的原因。

场景翻译为一个可执行的例子,以自动化测试的方式。运行测试,看着它失败,就具有了一个使之通过的清晰目标,只编写足够的生产代码---不要多写。将场景刻画为可执行的测试代码是一种设计行为,测试代码成为生产代码的调用者,使你在还没写任何代码之前就验证你的设计。用具体例子的形式来表达需求时一种强大的验证工具---我们只能通过将测试作为设计工具才能获取价值。

                                   2.其次,严格遵循规则,仅仅编写足以使测试通过的代码,会保持设计简单并适合目的。解决根本性复杂中引入的偶发性复杂(根本复杂性(essential complexity)指问题与生俱来的、无法避免的困难。偶发复杂性(accidental complexity)是人们解决根本复杂性的过程中衍生的。)测试指出缺陷或代码中缺失的功能,只写足以使测试通过的代码,严格地向简单设计来重构,这三者的组合极其强大,可以将偶发复杂性消灭在萌芽中。


       (3)   TDD的五个原则

  1. 独立测试:不同代码的测试应该相互独立,一个类对应一个测试类,一个函数对应一个测试函数。用例也应各自独立,每个用例不能使用其他用例的结果数据,结果也不能依赖于用例执行顺序。 一个角色:开发过程包含多种工作,如:编写测试代码、编写产品代码、代码重构等。做不同的工作时,应专注于当前的角色,不要过多考虑其他方面的细节。
  2. 测试列表:代码的功能点可能很多,并且需求可能是陆续出现的,任何阶段想添加功能时,应把相关功能点加到测试列表中,然后才能继续手头工作,避免疏漏。
  3. 测试驱动:即利用测试来驱动开发,是TDD的核心。要实现某个功能,要编写某个类或某个函数,应首先编写测试代码,明确这个类、这个函数如何使用,如何测试,然后在对其进行设计、编码。
  4. 先写断言:编写测试代码时,应该首先编写判断代码功能的断言语句,然后编写必要的辅助语句。
  5. 可测试性:产品代码设计、开发时的应尽可能提高可测试性。每个代码单元的功能应该比较单纯,“各家自扫门前雪”,每个类、每个函数应该只做它该做的事,不要弄成大杂烩。尤其是增加新功能时,不要为了图一时之便,随便在原有代码中添加功能,对于C++编程,应多考虑使用子类、继承、重载等OO方法。
  6. 及时重构:对结构不合理,重复等“味道”不好的代码,在测试通过后,应及时进行重构。
  7. 小步前进:软件开发是复杂性非常高的工作,小步前进是降低复杂性的好办法。

     

        总结:写一小段某个功能的测试代码,测试失败,再写实现代码,测试成功,再迭代下一个功能。

      (4)TDD的不足之处

  • 测试代码与需求的符合问题解决得不是很好,非技术人员、客户看不懂代码,无法评审测试是否符合需求。
  • 写得太大或者太小,令开发人员效率下降。这与测试代码与功能对应不起来有很大关系。

 

2.BDD    行为驱动开发(Behavior Driven Development)

              

         1.它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。主要是从用户的需求出发,强调系统行为。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。

         2.软件行业存在的问题:             

                                                          (1)需求和开发脱节:    用户想要的功能没有开发   开发的功能并非用户想要   用户和开发人员所说语言不同

                                                          (2)开发和测试脱节:    开发和测试被认为割裂       从开发到测试周期过长       测试自动化程度低

         简单来讲  就是 需求 ,开发 测试三者通过伪代码的形式来明确需求

                          参考篇文章         https://www.zhihu.com/question/20161970/answer/1341811526

二.怎样的测试才算优秀

            

       (1)测试代码的可读性和可维护性

测试代码难以理解,要多花时间才能明白测试的意图,如果测试出错后要调查代码来搞清情况。

       (2)代码在项目中及特定源代码中的组织方式

简洁目的明确得     

      (3)几乎不会失败的测试等于废物

                    如果测试从不失败或者一直失败,那么就没价值。

      (4)测试可靠性和可重复性

                     隔离和独立很重要,因为没有它们就难以运行和维护测试。

   

测试替身替换掉第三方依赖库,根据需要将其包装到你自己的适配层中。将各种麻烦封装进适配层以后,你就可以独立测试其余的程序逻辑。

测试代码与其用到的资源放在一起,或许是在一个包内。

测试代码自己生产所需资源,而不是让他们与源代码分开。

测试自行建立所需上下文,而不要依赖于之前运行的测试。(测试保证无序,即每个测试代码之间是独立的无序得)

持久化的集成测试,那就使用内存数据库(干净的数据集,能极大简化测试的启动问题,并且通常启动的超级快)。

线程代码分为同步和异步两部分,所有程序逻辑都放在一个常规的同步代码单元中,就可以方便的进行测试并且没有并发症,将棘手的并发部分留给一小堆专用测试。

      (5)测试对测试替身的使用

                测试替身是程序员所熟知的stub桩、fake伪造对象、mock模拟对象的总称。

测试目的、用于替换真实协作者的对象。它促进了改进:通过简化要执行的代码来加速执行测试;模拟难以出现的异常情况;观察那些对测试代码不可见的状态和交互。

                      stub存在的意图是为了让测试对象可以正常的执行,其实现一般会硬编码一些输入和输出

 

 三.测试替身

为什么要mock?

 

使你获得对环境的完整控制,从而在其中测试你的代码。你有效的将被测代码与其协作者隔离开,以便进行测试。

                   个人理解:在不修改源码,不改动逻辑的基础上,将被测代码隔离出来

                      (1)隔离被测代码

                      (2)加速执行测试

                     (3)使执行变得确定

                                 测试就是指定行为,并验证行为符合规范。只要代码具有完全确定性,并且其逻辑不包含一丝随机性,这就是简单而直接的。为了使代码具有完全确定性,你就需要能够针对同样的代码重复地运行测试,并总是得到相同的结果。

                     (4)模拟特殊情况  

                    (5)暴露隐藏的信息        

                       

         2、测试替身的类型

                         

单元测试学习笔记(一)_模块化_02

                      桩,截断的或非常短的物体。测试桩的目的是用最简单的可能实现来代替真实实现。最基本的例子是一个对象的所有方法都只有一行,且他们各自返回一个适当的默认值。

         3.使用测试替身的指南   

 

即两个对象之间的方法调用,你可能会需要一个模拟对象mock。

决定使用mock,但测试代码最终看起来不像你想象的那么漂亮,那就看看一个手工的简单测试间谍spy能否满足需要。

协作对象向被测对象输送的响应,用桩stub就可以。

想运行一个复杂场景,其中它所依赖的服务或组件无法供测试使用,而你对所有交互打桩的快速尝试却戛然而止,或产出了难以维护的糟糕的测试代码。那就实现一个伪造对象fake吧。

         e)如果上述都不行,那就抛硬币吧。

stub管查询,mock管操作。

 

         4.准备、执行、断言


准备、执行、断言(arrange-act-assert)。

         

单元测试学习笔记(一)_子类_03

           三个部分之间留空白,用来强调三段代码的不同角色。这种结构相当普遍,有助于使测试保持专注。如果感觉三部分中某一部分很“大”,那就是一个信号,表明测试可能试图做太多事情,需要更加专注。  

 

       5.什么是可测试的业务代码规则

            

                第一,模块化设计。设计由不同模块组合而成,每一个都服务于设计中的一个特定目的,正是这种性质使得设计变得模块化。通过将程序的整体功能分解为不同的责任,并指派给单独的组件,我们最终得到一个非常灵活的设计。

       每个单独的模块包含了满足自身功能所需的一切(自包含),通过将这些分离的模块组合成整体设计,我们引入了各种接缝,并从中构建出灵活性。这种编程风格强调模块之间的依赖应尽量少(松耦合)。

       由小模块来构建软件有助于大产品团队中队员之间的协作,因为产品的功能性变化往往更多的被隔离在代码的特定部分。这遵循了模块化设计的特征,系统被分离为功能元素,其相应的责任承载于特定的功能和能力上。

       这种受到模块化设计启发的结构能使系统逐渐扩展,只要模块本身足够的自包含和松耦合,那么仅仅需要插入新模块就能改变或新增功能,而功能关注点分散在代码中。这也直接有助于可测性,因为模块化设计的属性也就是使代码可测的属性。

 

              

                第二,SOLID设计原则。面向对象原则的好处在于,它们与可测性紧密结合。保持你的代码遵循SOLID设计原则,你就更有可能得到一个模块化的设计。

           a)单一职责原则(single responsibility principle,SRP),是指“类发生变化的原因应该只有一个”。也就是说,类要小而专注,并具有高内聚。方法也应该只有一个变化的原因。遵循SRP的代码容易理解和处理,反过来也容易测试,因为编写测试本质上是在指定期望的行为,表达对代码要解决的问题的理解。

           b)开闭原则(open-closed principle,OCP),类应当“对扩展开发,对修改关闭”。其实就是说在不改变源代码的情况下去改变类的行为,例如替换不同策略。类将具体责任委托给其他对象,可以使得测试在需要模拟具体场景时替换一个测试替身。

           c)里氏替换原则(liskov substitution principle,LSP),“子类应该能替换掉父类”。遵循LSP的类继承关系通过使用契约测试(contract test)来提高可测性---为一个接口编写的测试,可以用于该接口的所有实现。 

           d)接口隔离原则(interface segregation principle,ISP)“许多具体的客户接口要好过一个宽泛的接口”。简言之,你要保持小而专注。小接口改善可测性的方式,就是使其更容易编写测试和使用测试替身。例如,一个测试可能希望对协作者A打桩,假冒协作者B,将协作者C替换为间谍。如果每个协作者拥有自己的小接口的话,就可以直接去实现测试替身。

           e)依赖反转原则(dependency inversion principle,DIP),“应该依赖于抽象,而不是细节”。极端的说,DIP认为类不应该实例化自己的协作者,而是传入接口。依赖注入是可测性的福音,因为不仅可以替换协作者,而且可以节约工作量。因为测试使用代码的方式,和生产环境一样。

          第三,上下文中的模块化设计。“不仅需要确保你的系统是按照模块化来设计的。同时也要认识到,无论系统多么巨大和美好,应该总是将其设计成另一个更大系统的一部分

          第四,以测试驱动出模块化设计。用测试来驱动代码,是借鉴模块化设计的最快手段。实现代码前先写测试,本来就是一种确保你从客户视角来塑造代码的方式。也就是说你得到的设计更加有可能满足目标,而且设计的可测性是不成问题的。

 

       6、可测的设计的指南

 

          

       (1)避免复杂的私有方法

        没有什么好办法来测试private方法,应该尽量避免直接测试private方法。

        不是不测,而是尽量避免。只要那些方法短小好记,并有助于public方法更容易阅读,那么仅通过public方法来测试就没有问题了。

         当private方法不那么直接了当,并且你觉得需要为之写测试时,应该重构代码,将private方法封装的逻辑转移到另一个对象中,成为public方法。

       (2)避免final方法

         很少有程序需要final方法,而你其实也并不需要它。将方法声明为final的主要目的是确保它不会被子类覆盖。“将类声明为final,可以阻止恶意的子类去添加终结器(finalizer)、复制和重载随机方法。”

          尽管在上下文中成立,但是不见得你就应该使用final修饰符。该逻辑有两个问题:第一,那些潜在的子类可能就是你身边的人写的;第二,反射API可以用于去除final修饰符。实际上,将方法声明为final的唯一合理情形就是,当你在运行期加载外部类的时候,或者你不信任同事(这有个更大的问题需要担心)。

         最大的问题在于:final关键字是否妨碍了你的测试。如果是的话,权衡一下糟糕的可测性带来的负担与使用final带来的好处,孰轻孰重。

         性能:由于final方法无法被覆盖,编译器可以内联inline该方法的实现来优化代码。但是“我不会纯粹因为性能原因而将方法或类声明为final,只有当你真的识别出性能问题之后,再去考虑它。”

       (3)避免static方法

          大多数static方法是不必要的。写这种方法,一是因为方法与特定实例无关,或者静态方法更容易进行全局访问。

          头号规则:对于某个方法,如果你预见到你将会在单元测试中为其打桩,那么就不要将其声明为static。

       (4)使用new时要当心

          关键字new是最常见的硬编码方式。如果你想在测试时为一个对象替换为测试替身,那么就不要用new在方法中实例化它,而应该作为参数被传入那个方法。

       (5)避免在构造函数中包含逻辑

           构造函数很难绕过去,因为子类的构造函数至少会触发超类至少一个的构造函数。因此,应当避免在构造函数中放置严重需要测试的代码或逻辑。更好的方式是,将所有代码抽取到可以被子类覆盖的protected方法中。

           总之,对于构造函数中的任何代码,确保你不会想要在单元测试中替换它们。如果发现了这种代码,最好将其移动到某个方法中,或者移动到某个你可以覆盖的方法中,或者参数对象中。

      (6)避免单例

          单例模式带来的烂代码耗费了软件工业数以百万级的美元。它曾是一种“确保类只有一个实例,并提供全局访问点”而设计出来得模式。在我看来,你根本不需要它。

          的确在有些情况下,你需要在运行期保留类的一个实例。但是单例模式往往也妨碍了测试区创建不同的变体。要想测试,得通过反射注入新实例,或者通过setter方法注入。

          最佳和更为可测的设计,是不强制唯一实例的单例,而是依赖于程序员的“我们只会在生产环境中实例化出一个对象”的约定。毕竟,如果你仍然需要防范猪一般的队友,那么有更大的麻烦在等着你。

      (7)组合优于继承。

          为了重用而继承,就好比杀鸡用牛刀。继承允许重用代码,但是也带来了严格的类继承关系,抑制了可测性。“继承的关键在于利用多态行为而非重用代码。成为一个类的子类,就不能再成为其他类的子类,将永远将构造函数固定在那个父类上,父类改变API时只能任其摆布,在编译期就已失去了变化的自由。而组合给了多种选择,可以重用不同的实现(本该重用父类方法的),可以在运行期改变主意。”

      (8)封装外部库

         不是所有人都像你一样擅长提出可测的设计。请谨慎地继承第三方外部库。从外部库继承更糟,因为你很难控制被继承的代码。记得自行设计一套测试友好的接口,用其将外部库包裹起来,以便容易的替换掉实际的实现。(将不可测的代码包裹在薄薄一层可测的代码之中)

     (9)避免服务查找

        大多数服务查找(比如调用静态方法来获取单例实例),是在看似干净的接口与可测性之间所做的糟糕权衡。接口只是看起来干净,因为那些无法明确作为构造函数参数的依赖,现在却隐藏在类中。在测试中替换那依赖从技术上讲不是不可能,但是需要更多的工作才能做到。