测试驱动开发_测试驱动开发.pdf - CSDN
精华内容
参与话题
  • 浅谈测试驱动开发(TDD)

    千次阅读 2016-04-17 09:12:46
    浅谈测试驱动开发(TDD) 测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量。本文从开发人员使用的角度,介绍了 TDD 优势、原理、过程、原则、测试技术...

    浅谈测试驱动开发(TDD)

    测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量。本文从开发人员使用的角度,介绍了 TDD 优势、原理、过程、原则、测试技术、Tips 等方面。

    李群 (liqun@nsfocus.com)www.ihere.org

    李群当前关注于网络安全产品的开发、研究;软件开发过程等方面。您可以通过 liqun@nsfocus.com和他联系。



    2004 年 11 月 19 日

    背景

    一个高效的软件开发过程对软件开发人员来说是至关重要的,决定着开发是痛苦的挣扎,还是不断进步的喜悦。国人对软件蓝领的不屑,对繁琐冗长的传统开发过程的不耐,使大多数开发人员无所适从。最近兴起的一些软件开发过程相关的技术,提供一些比较高效、实用的软件过程开发方法。其中比较基础、关键的一个技术就是测试驱动开发(Test-Driven Development)。虽然TDD光大于极限编程,但测试驱动开发完全可以单独应用。下面就从开发人员使用的角度进行介绍,使开发人员用最少的代价尽快理解、掌握、应用这种技术。下面分优势,原理,过程,原则,测试技术,Tips等方面进行讨论。


    1. 优势

    TDD的基本思路就是通过测试来推动整个开发的进行。而测试驱动开发技术并不只是单纯的测试工作。

    需求向来就是软件开发过程中感觉最不好明确描述、易变的东西。这里说的需求不只是指用户的需求,还包括对代码的使用需求。很多开发人员最害怕的就是后期还要修改某个类或者函数的接口进行修改或者扩展,为什么会发生这样的事情就是因为这部分代码的使用需求没有很好的描述。测试驱动开发就是通过编写测试用例,先考虑代码的使用需求(包括功能、过程、接口等),而且这个描述是无二义的,可执行验证的。

    通过编写这部分代码的测试用例,对其功能的分解、使用过程、接口都进行了设计。而且这种从使用角度对代码的设计通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。因此测试驱动开发也是一种代码设计的过程。

    开发人员通常对编写文档非常厌烦,但要使用、理解别人的代码时通常又希望能有文档进行指导。而测试驱动开发过程中产生的测试用例代码就是对代码的最好的解释。

    快乐工作的基础就是对自己有信心,对自己的工作成果有信心。当前很多开发人员却经常在担心:“代码是否正确?”“辛苦编写的代码还有没有严重bug?”“修改的新代码对其他部分有没有影响?”。这种担心甚至导致某些代码应该修改却不敢修改的地步。测试驱动开发提供的测试集就可以作为你信心的来源。

    当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位bug。而迅速发现、定位bug是很多开发人员的梦想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位bug提供了条件。

    我的一段功能非常复杂的代码使用TDD开发完成,真实环境应用中只发现几个bug,而且很快被定位解决。您在应用后,也一定会为那种自信的开发过程,功能不断增加、完善的感觉,迅速发现、定位bug的能力所感染,喜欢这个技术的。

    那么是什么样的原理、方法提供上面说的这些好处哪?下面我们就看看TDD的原理。


    2. 原理

    测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完全部功能的开发。

    我们这里把这个技术的应用领域从代码编写扩展到整个开发过程。应该对整个开发过程的各个阶段进行测试驱动,首先思考如何对这个阶段进行测试、验证、考核,并编写相关的测试文档,然后开始下一步工作,最后再验证相关的工作。下图是一个比较流行的测试模型:V测试模型。

    【图 V测试模型】
    【图 V测试模型】

    在开发的各个阶段,包括需求分析、概要设计、详细设计、编码过程中都应该考虑相对应的测试工作,完成相关的测试用例的设计、测试方案、测试计划的编写。这里提到的开发阶段只是举例,根据实际的开发活动进行调整。相关的测试文档也不一定是非常详细复杂的文档,或者什么形式,但应该养成测试驱动的习惯。

    关于测试模型,还有X测试模型。这个测试模型,我认为,是对详细阶段和编码阶段进行建模,应该说更详细的描述了详细设计和编码阶段的开发行为。及针对某个功能进行对应的测试驱动开发。

    【图 X测试模型】
    【图 X测试模型】

    基本原理应该说非常简单,那么如何进行实际操作哪,下面对开发过程进行详细的介绍。


    3. 过程

    软件开发其他阶段的测试驱动开发,根据测试驱动开发的思想完成对应的测试文档即可。下面针对详细设计和编码阶段进行介绍。

    测试驱动开发的基本过程如下:

    1) 明确当前要完成的功能。可以记录成一个 TODO 列表。

    2) 快速完成针对此功能的测试用例编写。

    3) 测试代码编译不通过。

    4) 编写对应的功能代码。

    5) 测试通过。

    6) 对代码进行重构,并保证测试通过。

    7) 循环完成所有功能的开发。

    为了保证整个测试过程比较快捷、方便,通常可以使用测试框架组织所有的测试用例。一个免费的、优秀的测试框架是 Xunit 系列,几乎所有的语言都有对应的测试框架。我曾经写过一篇文章介绍CppUnit的文章( http://www.ibm.com/developerworks/cn/linux/l-cppunit/index.html)。

    开发过程中,通常把测试代码和功能代码分开存放,这里提供一个简单的测试框架使用例子,您可以通过它了解测试框架的使用。下面是文件列表。

    	project/				项目主目录
    	project/test			测试项目主目录
    	project/test/testSeq.cpp		测试seq_t 的测试文件,对其他功能文件的测试文件复制后修改即可
    	project/test/testSeq.h
    	project/test/Makefile			测试项目的 Makefile 
    	project/test/main.cpp			测试项目的主文件,不需要修改
    	project/main.cpp		           项目的主文件
    	project/seq_t.h			功能代码,被测试文件
    	project/Makefile		           项目的 Makefile

    主要流程基本如此,但要让你的代码很容易的进行测试,全面又不繁琐的进行测试,还是有很多测试原则和技术需要考虑。


    4. 原则

    测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。

    一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。

    测试列表。需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。

    测试驱动。这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。

    先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。

    可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。

    及时重构。无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。关于重构,我会另撰文详细分析。

    小步前进。软件开发是个复杂性非常高的工作,开发过程中要考虑很多东西,包括代码的正确性、可扩展性、性能等等,很多问题都是因为复杂性太大导致的。极限编程提出了一个非常好的思路就是小步前进。把所有的规模大、复杂性高的工作,分解成小的任务来完成。对于一个类来说,一个功能一个功能的完成,如果太困难就再分解。每个功能的完成就走测试代码-功能代码-测试-重构的循环。通过分解降低整个系统开发的复杂性。这样的效果非常明显。几个小的功能代码完成后,大的功能代码几乎是不用调试就可以通过。一个个类方法的实现,很快就看到整个类很快就完成啦。本来感觉很多特性需要增加,很快就会看到没有几个啦。你甚至会为这个速度感到震惊。(我理解,是大幅度减少调试、出错的时间产生的这种速度感)


    5. 测试技术

    5.1. 测试范围、粒度

    对哪些功能进行测试?会不会太繁琐?什么时候可以停止测试?这些问题比较常见。按大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。感到疲劳就应该停下来休息一下。感觉没有必要更详细的测试,就停止本轮测试。

    测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据你的经验,功能复杂、核心功能的代码就应该编写更全面、细致的测试用例,否则测试流程即可。

    测试范围没有静态的标准,同时也应该可以随着时间改变。对于开始没有编写足够的测试的功能代码,随着bug的出现,根据bug补齐相关的测试用例即可。

    小步前进的原则,要求我们对大的功能块测试时,应该先分拆成更小的功能块进行测试,比如一个类A使用了类B、C,就应该编写到A使用B、C功能的测试代码前,完成对B、C的测试和开发。那么是不是每个小类或者小函数都应该测试哪?我认为没有必要。你应该运用你的经验,对那些可能出问题的地方重点测试,感觉不可能出问题的地方就等它真正出问题的时候再补测试吧。

    5.2. 怎么编写测试用例

    测试用例的编写就用上了传统的测试技术。

    • 操作过程尽量模拟正常使用的过程。
    • 全面的测试用例应该尽量做到分支覆盖,核心代码尽量做到路径覆盖。
    • 测试数据尽量包括:真实数据、边界数据。
    • 测试语句和测试数据应该尽量简单,容易理解。
    • 为了避免对其他代码过多的依赖,可以实现简单的桩函数或桩类(Mock Object)。
    • 如果内部状态非常复杂或者应该判断流程而不是状态,可以通过记录日志字符串的方式进行验证。

    6. Tips

    很多朋友有疑问,“测试代码的正确性如何保障?是写测试代码还是写测试文档?”这样是不是会陷入“鸡生蛋,蛋生鸡”的循环。其实是不会的。通常测试代码通常是非常简单的,通常围绕着某个情况的正确性判断的几个语句,如果太复杂,就应该继续分解啦。而传统的开发过程通常强调测试文档。但随着开发节奏的加快,用户需求的不断变化,维护高层(需求、概要设计)的测试文档可以,更低层的测试文档的成本的确太大了。而且可实时验证功能正确性的测试代码就是对代码最好的文档。

    软件开发过程中,除了遵守上面提到的测试驱动开发的几个原则外,一个需要注意的问题就是,谨防过度设计。编写功能代码时应该关注于完成当前功能点,通过测试,使用最简单、直接的方式来编码。过多的考虑后期的扩展,其他功能的添加,无疑增加了过多的复杂性,容易产生问题。应该等到要添加这些特性时在进行详细的测试驱动开发。到时候,有整套测试用例做基础,通过不断重构很容易添加相关特性。

    参考资料


    http://www.ibm.com/developerworks/cn/linux/l-tdd/

    展开全文
  • 测试驱动开发,单元测试,工程实践、极限编程、敏捷开发
  • 测试驱动开发的艺术电子完整版,内容完整,清晰,有目录,完整代码和详细的步骤
  • 说说测试驱动开发

    千次阅读 2017-04-28 10:53:22
    1 测试驱动开发模式1.1 重新定义“测试”这是一张影响图: 普通箭头表示当第一个节点增长时,第二个节点也会做相应的增长。 带圆圈的箭头表示当第一个节点增长时,第二个节点也会做相应的减少。 当压力越大时,所做...

    1 测试驱动开发模式

    1.1 重新定义“测试”

    这是一张影响图:

    • 普通箭头表示当第一个节点增长时,第二个节点也会做相应的增长。
    • 带圆圈的箭头表示当第一个节点增长时,第二个节点也会做相应的减少。

    当压力越大时,所做的测试就会越少。测试越少,犯的错就会越多,就会感到更大的压力。这是一个会造成情境越来越糟的循环。

    我们用事先编写的测试来驱动开发,因为测试先于开发,所以我们在感到压力时,就运行这些测试,它们会马上给我们一种系统良好的感觉,而且会减少开发出错的次数,进而减少我们的压力,从而跳出上面的循环。

    1.2 测试的要求

    1. 让测试尽可能快地运行。
    2. 尽量在小范围内进行。
    3. 测试之间互不干扰,这意味着所有的测试都是不依赖于顺序的。这样就不会因为测试顺序的不同而出现的许多古怪问题。这对开发人员提出了更高的要求,因为必须把问题分解为彼此正交的小问题,这样做的好处是,每个测试会变得更加简单而且可以快速运行,对象也会变成漂亮的“高内聚、低耦合”的类对象。

    1.3 测试列表

    写一张包含所有要编写的测试清单,可以记录在一张纸上。这张表记录的就是我们要去实现的测试。

    1.4 测试优先

    在编写要被测试的代码之前也编写测试代码。

    1.5 断言优先

    从测试完成时能够通过的断言(形如 assertEquals())开始编写测试代码。

    1.6 测试数据

    使用让人容易理解的数据,记住,你写的代码以后会有人阅读、有人维护的!

    在以下的情况下,使用真实世界中的数据很有效:

    • 将目前系统的输出与以前系统的数据进行比对时。
    • 对旧系统进行重构而期望在完成时得到完全相同的结果时。

    1.7 可阅读的数据

    让测试包含预期和实际的结果,努力让它们容易理解,因为还要让其他人阅读的,所以要留下尽可能多的线索。

    1.8 学习测试

    使用一个公开的 API 接口或者包时,可以编写一个测试来验证这个 API 工作是否符合我们的期望。这样做有两点好处:
    * 可以让我们更好地理解它。
    * 如果测试无法运行,可能是使用的问题,也可能是 API 本身的问题,测试会让问题更快地暴露出来。

    1.9 休息

    当你感觉到累的时候,休息一下。适当的放松会让大脑的思维得到解放。

    2 测试代码的编码模式

    2.1 子测试

    保持不可运行-》可运行-》重构的节奏对可持续的成功非常重要。付出额外的努力来保持这种节奏是值得的。当之前的测试代码突然要求几处变化时,可能的原因是我们之前所写的测试代码太大了,这就需要重构把这些测试代码,尝试把它变成几个子测试代码咯。

    2.2 模拟对象

    如果一个测试依赖于昂贵而且复制的资源对象,那么就创建一个这些资源的模拟对象。典型的例子是数据库,建立它很耗时间,而且它也是开发过程中错误产生的温床。

    解决办法是在大多数时间里不使用真正的数据库,而是写一个像数据库一样的对象,或者采用一个第三方开源的数据库模拟 API,让它仅仅驻留在内存中。这样做,不仅带来了高效的性能,而且还很可靠,可读性也很好。

    如果担心模拟对象与现实对象的行为不一致,那么可以使用对实际对象适用的一系列测试来测试模拟对象,从而减少这种风险。

    2.3 清理测试死角

    抛出一个异常对象来测试代码中处理错误的逻辑。因为我们的安全假设是,没有被测试过的代码是不会正常工作的,处理错误的逻辑也是代码的一部分,所以我们也要进行测试。

    假设要测试当文件系统满了以后,系统会发生什么。可以花费很多时间创建许多大文件来填满整个文件系统,这种方法费时费力。另一种方法是采用伪实现,即抛出一个异常:

    public class FullFile extends File {
        public FullFile(String path){
            super(path);
        }
        public boolean createNewFile() throws IOException {
            throw new IOException();
        }
    }

    2.4 提交代码前确保所有的测试都运行通过

    如果在试图提交时,发现有许多测试没有通过。最可能的原因是你可能对刚刚编码的东西没有完全理解,因此最简单的办法就是把之前做过的工作推倒重来。

    因为许多测试没有通过,注释掉一些测试代码是严格禁止的,这是一种不负责任的、偷懒的表现。

    3 可运行模式

    3.1 伪实现

    测试不能通过的时候,先返回一个常量,让测试运行通过。然后通过重构把这个常量逐步转换为用变量表示的表达式。伪实现就像登山时在头顶上方钉一个登山用的钢锥,它会让你安心,让你觉得继续爬上去是安全的。

    使用伪实现有两个主要原因:

    • 当状态是可运行的时候,我们就能够从这里充满自信地开始重构。
    • 从一个具体的测试用例开始,聚焦于一点,避免其他干扰,我们就能更好地解决当下问题。

    3.2 三角法

    三角法的含义是:只有当我们有有了两个或两个以上的测试用例时,才能对功能进行抽象。

    只有在不能确定是否需要对要实现的功能是否已经进行了正确的抽象时,才使用这个方法。

    3.3 显明实现

    显明实现含义是:如果我们知道要些什么,并且能够很快地完成,那就直接实现吧。

    显明实现只是第二选择。因为你在追求自身的完美,而这是不可能的!所以记住时刻保持不可运行-》可运行-》重构的编码习惯!

    4 xUnit 测试框架模式

    4.1 断言

    写一个布尔表达式传递给一个断言,来判断我们的工作是否运行正常。形如assertEquals(...)

    在 JUnit 中,可以为断言传递更详细的信息,形如 assertTrue("测试用例 1",false)

    4.2 初始化固定设施(脚手架)

    如果几个测试都存在一些通用对象,这种重复是不好的,因为:

    • 它需要花费一些时间去编写(复制、粘贴),而我们是期望能够实现快速编写。
    • 如果要修改对这个通用对象进行修改,那么这些测试中的相应的代码都需要修改。这时一种重复工作,应该避免。

    所以我们把几个测试都需要的通用对象,转变为实例变量,然后在 setUp() 中初始化这些对象。

    4.3 释放固定设施(脚手架)

    使用 tearDown 来释放资源,这样能避免重复,原因和初始化固定设施是一样的!

    不论测试代码中发生了什么,甚至是抛出一个异常,xUnit 也能保证最后调用 tearDown

    4.4 异常测试

    我们需要测试期望的异常,只有捕获到这些异常,异常测试才算通过。

    我们只关心捕获我们所期望的异常,因此,如果抛出了一个非期望的异常,我们也会从 xUnit 框架中得到通知。

    4.5 全部测试

    可以把所有的测试合成一个测试套件,每个包一个套件,一次性执行所有的测试。

    5 实践

    5.1 实践的步伐

    一般经过一段时间的适应,开发人员都会倾向于采用小步骤进行开发。可以利用 IDE 的自动重构功能来提高重构的速度。

    5.2 测试的内容

    应该测试这些东西:

    • 条件部分
    • 循环部分
    • 操作部分
    • 多态性

    记住,只测试我们编写的代码,除非有足够的理由,否则不要测试其他来源的代码。

    5.3 设计存在的缺陷

    这些是设计存在的缺陷的特征:

    • 过长的 setUp 代码:一个简单的断言,需要花费上百行的代码创建对象。肯定是对象太大了,需要分割。
    • 冗余的 setUp 代码:无法为这些相同的公共设施的代码找一个存放它的一个统一的地方,这说明有太多的对象紧密地联系在一起咯。
    • 过长的测试运行时间。
    • 意外中断的测试:说明测试的一部分对另一部分产生了影响,这是一个脆弱的测试。我们需要修改设计,要么打破联系,要么合并它们,消除这种影响。

    5.4 编写测试的数量

    • 通过自己的经验和判断来决定要编写多少个测试。
    • 一个测试是否值得编写,取决于对功能的平均无故障时间的理解。如果一个功能可能用上 100 年,那么针对那些极不可能发生条件和条件组合编写测试就是一件有意义的事。
    • 注重实效,我们编写测试是为了充满自信地编写代码。

    5.5 删除测试

    如果存在两个测试互为冗余,根据以下原则进行删除:

    • 如果删除一个测试降低了我们对于整个系统的信心,那么就保留它。
    • 如果这两个测试对用户来说,是两种不同的情形,那么就保留它。
    • 如果上述两种情况都不存在,那么就删除其中用处最少的那个。

    5.6 开发大型系统案例

    LifeWare 的一个跟保险有关项目就是采用 TDD 进行开发的。共历时 4 年,有 40 人参与了这个项目。编写了大约 500,000 行代码(其中有一半是测试代码),有 4000 个能够在 20 分钟内能够执行完毕的测试。所以现在有信心了吧 O(∩_∩)O~

    5.7 项目中途采用 TDD

    • 这些代码是在没有考虑测试的情况下写出来的,所以它们的可测试性不强。所以我们必须严格限定修改的范围,一步一步来。
    • 如果发现系统中的某一部分可以显著地进行简化,但目前还没有这样的要求时,就不要动它。

    5.8 TDD 有效性

    • 我们越早发现并处理一个错误,我们所付出的成本就越低。
    • 很多应用了 TDD 的团队,开发人员的精神变得更加放松,团队成员之间都提高了信任度,用户也开始期待新的项目版本咯。
    • TDD 缩短了设计决策的反馈循环。
    展开全文
  • 测试驱动开发

    千次阅读 2019-06-20 12:42:58
    测试驱动开发 概述 极限编程是一个轻量级的、灵巧的软件开发方法,同时它也是一个非常严 谨和周密的方法,它从 4 个基本方面对软件项目进行改善:交流、简易、反馈 和勇气。测试驱动开发则是极限编程的最佳...

    测试驱动开发

    概述

    极限编程是一个轻量级的、灵巧的软件开发方法,同时它也是一个非常严

    谨和周密的方法,它从 4 个基本方面对软件项目进行改善:交流、简易、反馈

    和勇气。测试驱动开发则是极限编程的最佳实践之一。它是编程时使用的技术,

    要求在编写任何产品代码之前,首先编写用于定义产品代码行为的测试。采用

    测试驱动开发,我们将得到简单、清晰、高质量的代码。 MVC 模式是一个复杂的架构模式,它将一个应用的输入、处理、输出流程按照 Model、View、Controller 的方式进行分离,使得产品的结构清晰,易于 维护,有利于软件工程化管理。 Kent Beck 作为极限编程的创始人,提出了测试驱动开发的部分方法,并成功地应用于许多小型的项目中。但是,测试驱动开发在许多系统中应用还存在一定的难度,比如具有图形用户界面和多层架构的系统。所以,基于设计模式的测试驱动开发的研究成为国内外软件工程方向研究者们的课题之一。

    反馈是 XP 的四个基本的价值观之一——在软件开发中,只有通过充分的测试才能获得充分的反馈。XP 中提出的测试,在其它软件开发方法中都可以见到,比如功能测试、单元测试、系统测试和负荷测试等;与众不同的是,XP 将测试结合到它独特的螺旋式增量型开发过程中,测试随着项目的进展而不断积累。另外,由于强调整个开发小组拥有代码,测试也是由大家共同维护的。即,任何人在往代码库中放程序前,都应该运行一遍所有的测试;任何人如果发现了一个 BUG,都应该立即为这个 BUG 增加一个测试,而不是等待写那个程序的人来完成;任何人接手其他人的任务,或者修改其他人的代码和设计,改动完以后如果能通过所有测试,就证明他的工作没有破坏原系统。这样,测试才能真正起到帮助获得反馈的作用;而且,通过不断地优先编写和累积,测试应该可以基本覆盖全部的客户和开发需求,因此开发人员和客户可以得到尽可能充足的反馈。

        测试驱动开发(Test- Driven Development), 简称TDD, 由Kent Beck 提出的一种软件开发方式。测试驱动开发以测试作为开发过程的中心, 它要求在编写任何产品代码之前, 首先编写用于定义产品代码行为的测试, 而编写的产品代码又要以使测试通过为目标。测试驱动开发要求测试可以完全自动化的运行, 在对代码进行重构前后必须运行测试。测试驱动开发主要包括两方面:测试先行和代码重构。测试主要针对单元(最小的可测试软件元素)实施测试。它所测试的内容包括内部结构(如逻辑和数据流)以及单元的功能和可观测的行为。测试先行一改传统开发模式的单元测试在编写代码之后进行, 而将单元测试的编写移至编写正式代码之前。重构是在不改变代码外在行为的条件下改进其内部的行为的一种软件系统改变的过程, 使代码松耦合度(对外界代码依赖低)并且内聚度高(内部只完成一项功能) 。测试驱动开发作为极限 编程思想的一种主要实践, 可以有效地让程序开发人员开发出更高品质的、经过完整测试的程序。测试驱动开发以测试作为开发过程的开端, 它要求在编写任何产品代码之前,首先编写用于定义产品代码行为的测试, 而编写的产品代码又要以使测试通过为目标。TDD 不是一种开发工具,也不是一种测试方法, 它是一种编码之前进行单元测试的软件开发思想。

     

    1. 测试驱动开发的研究与实践

    测试驱动开发流程:TDD 开发过程有别于传统开发流程(Waterfall), 它在进行简单的概要设计后, 首先进行的是测试用例的编写,然后执行测试用例进行测试。测试失败, 则进行编码驱使测试通过, 这就是所谓的测试驱动。最终, 测试得到通过,再对代码进行重构,优化代码结构和性能。而传统流程则先进行概要设计, 然后在概要设计基础上进行详细设计,在详细设计阶段尽可能设想到全部问题和需求的解决方法, 然后才开始编码实现详细设计。TDD 开发流程图如图所示。

    测试驱动开发模式:在测试驱动开发中,关键问题如下:什么时候进行测试,如何选择要测试的逻辑和如何选择要测试的数据。测试驱动开发模式指导程序员如何解决上述问题。

    *测试相互独立。

    在测试驱动开发中, 所运行的各种测试之间关系的期望状态是没有任何相互影响的。相互独立的测试意味着所有的测试都是不依赖于顺序的, 可以随便从这些测试中挑出部分测试来运行。程序员必须将自己的问题分解为一些彼此正交的小问题, 这样就使得为每个测试搭建环境

    简单而快捷。独立测试鼓励利用高度内聚、低度耦合的对象组合来解决问题。

    *写出测试列表。

    程序员在开始写测试之前, 应该写一个包含所有必须要编写的测试的清单。那么, 记录到列表上的就是当前程序员要去实现的测试。首先将需要实现的每种操作的范例都记录在清单上。对于目前尚不存在的操作, 将其空版本记录在清单上。

    *测试和断言优先。

    在测试驱动开发中, 程序员构建一个系统应该是从其对最终系统的描述开始的。程序员应该从希望最终代码能够通过的测试开始编写一项功能。相应地,程序员应该从测试完成时能够通过的断言开始编写一个测试。在测试优先的测试里程序员应该尽量使用容易让人理解的数据, 一般不用一个常量来表达多种意思。

    一般从测试列表中选择具有指导意义并且比较有把握实现的测试来进行编写。当使用一个新类里的一种新的方法时, 不直接用它来编写程序, 而是编写一个小测试来验证这个API 的工作是否符合人们的愿望。当出现某种与当前讨论话题并不直接相关的想法时, 那么就在列表中增加一个测试然后重新回到论题上来。当发现一个错误的时候, 首先写一个尽可能小的测试并使其运行, 然后再去修复这个错误。

    利用JUnit进行测试驱动开发:在Eclipse 建立JUnit 测试, 并进行驱动开发。现在开 发一个"Hello Wor ld" 的例子。按照TDD 的规则, 应该在代码建立以前先把测试写好。为了能够在某处开始, 假设未来的类名是HelloWorld , 并且有一个方法Say(), 这个方法返回String 的值(例如"Hello World !")。根据设定的程序功能, 写出测试代码如下:

     

    import junit .framework.T estCase ;

    public class TestThatWeGetHelloWorldPrompt

    extends TestCase {

     public TestThatWeGetHelloWorldPrompt(

      String name){

       super(name);

      }

     public void testSay(){

     HelloWorld hi=new HelloWorld();

      assertEquals(" Hello World!" , hi.say());

     }

     public static void main(String[ ] args){

      junit.textui.TestRunner.run(

      TestThatWeGetHelloWorldPrompt.class);

     }

    }

    建立测试案例的步骤如下:

    1)建立一个junit.framework.TestCase的实例。

    2)定义一些以"test" 开头的无返回方法(例如test-WasTransactionSuccessful(), testS how(), 等等)。

    TestThatWeGetHelloWorldPrompt .java 包含这些:TestCase 的子类和一个叫做testSay()的方法。这个方法调用了assertEquals()函数, 它用来比较预期的值和由say()返回的值。main()方法用来运行测试和显示输出。JUnit 的TestRunner 处理测试, 提供基于图像和文本的输出表现形式。我们使用基于文本的版本, 因为Eclipse 支持它, 且也适合我们。当开始运行后, 基于文本的版本测试会以文本形式输出,Eclipse 会把这些输出自动变成图像界面的输出。

    现在建立被测试代码:

    public class HelloWorld {

     public String say(){

      return("Hello World!");

     }

    }

     

    1. 测试驱动开发在Java语言中的运用

    测试驱动开发与Java结合的实践应用:很多同学在程序开发过程中对测试不够重视,主要表现为:第一,没有针对实际问题设立出足够全面的测试用例;第二,主管认为某些代码是正确的,实际上无法为这些代码设计相应的测试用例。将测试驱动开发与Java课程有机结合起来,能够有效地客服同学们对测试认识的不足,从而编写出更高质量的程序。经过实践,可遵循以下步骤:

    首先编写测试用例:测试用例是对实际需求的现。用户向程序输入一个测试用例,则期望程序给出某些输出。输入和输出往往表现为函数关系。在传统的编程方式中,程序员也会考虑到实际需求问题,但往往由于仅限于思维上的没有做出清晰归纳的印象。尤其是一些心急的程序员,觉得写代码就是解决问题的全部工程。实际上这样忽略测试用例而编写出来的程序很容易未能覆盖实际问题的各方面要求。

    编写仅能通过测试用的代码:在编写测试用例阶段,首先要遵循的是:让编译器告知程序员,合适该增加新的方法,而不是主动对程序做出规划。在实际的Java语言课程教学实践中发现,有许多同学在针对某测试用例编写通过代码时,喜欢即兴发挥,写出一些并非通过本测试用例所需要的代码。这样可能会导致几个方面的问题:第一,这些代码无合适的测试用例验证,可能永远都不会被执行;第二,这些代码存在错误,但由于当前测试用例不是用于检验这些代码的,它们可能会遗漏到后期的软件开发中,甚至被集成到其它软件,造成后期难以检测出来的隐患;第三,这些代码可以解决问题的某一方面,但放置位置不合适,造成代

    码意义模糊。例如在上面的代码中,有些同学在完成了当前测试用例后,马上想到对于以文本文件方式输入的同类型的数据也可以使用同样的程序代码处理,于是在方法中加入有关文本文件的代码,这样的做法其实不合适,因为有关文本文件方式输入的处理应该在求最大公约数之前的方法中实现。

    代码重构:代码重构对于测试驱动开发来说相当重要。当程序中代码的代码越来越多,就有可能需要进行重构,以优化程序性能,并使程序更加"优美"。例如代码出现重复、某些代码要表达的意图过于复杂时,程序员都要考虑进行重构的必要性。代码重构与代码编写是交错进行的。代码编写是进行代码重构的基础,而适时地进行代码重构将能极大地加快代码编写的进度。另一方面,代码重构无可避免地增加了代码编写阶段的工作量,所以在进行重构的时机和范围都需要根据实际问题

    维护程序员测试集。进行增量式开发:对于测试集的编写,则应当遵循"不遗漏,不重复"的原则。通过前三个步骤,不断扩充测试用例至测试集,并立刻针对扩充的测试用例编写能通过的代码,直至测试集包括了实际问题的所有方面为止,则认为程序开发的代码编写阶段已经完成。

     

    1. 测试驱动开发在J2EE项目中实践

    J2EE是一种用来开发企业级软件应用系统的平台。目前针对J2EE项目,多采用多层架构设计,清晰简单、分工合作,每层使用特定的框架实现相应的设计策略。比如,Strum+Sprig+Hibernate就是一个很受欢迎的开源整合框架:表示层用Struts实现,业务层用Spring实现,持久层则用Hibernate实现。

    在这个整合框架下进行开发,对于普通水准的J2EE开发者来说,可能会有太多的时间被浪费在非关键任务上,或者是仅仅为了执行单元测试就不得不把所有程序部署到应用服务器上,其生产率是无法令人满意的。于是,一种全新的软件开发方法(测试驱动开发)就被提出且日益流行起来。

    JUnit工具介绍:作为流行成熟的回归测试框架,JUIlit提供了基于API的自动测试方法,您可以在测试代码中调用这个框架来检查条件是否满足,并报告错误的数量和类型。这种方案非常灵活,大多数情况下它大大减少了测试代码的维护时间,并且使应用中的复杂功能测试成为可能,还可交替使用白盒测试或黑盒测试。本文所讨论的测试工具有JMock、SpringContextTestCase、StrutsTestCase、Canoo WebTest,也都是JUnit在具体领域的扩展。

    项目组织结构:本项目采用的分层架构使Web应用达到了松散耦合还能灵活改变,并可以承载各种级别的业务处理,每层之间开放通信接口。各层都采用了测试先行编程的开发方法。

     

     

    src/dao(数据访问对象)目录存放持久层和域模型的实现;src/service目录存放业务层的实现;src/web目录存放表示层的实现。每层的测试用例则相应地存放在test目录下。

    Member类有四个属性,部分代码如下:

    持久层测试:

    该层测试使用了Spring对JUnit的扩展测试框架,AbstractTransactionalDataSourceSpringContextTests类的继承类MemberDaoTest可以不必依赖服务器而直接运行,从而对集成测试(类似单元测试)提供了良好的支持可以随意更新表中的数据而不必担心造成影响,因Spring的测试类支持事务管理,会自动圆滚在测试中所做的任何修改。按照TDD的骤接下来才是编写程序代码 ,创建MemberDao接口和相应的实现类MemberDao

    -Hibernate,然后在Spring配置文件中绑定。现在可以直接在Junit中进行测试。若测试通过,转到业务层开发。

    业务层测试: 我们使用的测试工具是JMock,它可以灵活地定义对象间交互时的约束关系,减少测试的脆弱性。

    使用JMock,需要继承MockObjeetTestCase类,先建立测试运行的上下文,然后设定Mock对象使用到方法、参数、返回值等行为,最后执行测试。按照测试先行方法,现在应该声明MemberManager接口并实现MemberManagerImpl类。

    表示层测试:Struts框架基于MVC(模型一视图一控制)设计模式,将数据访问、页面显示、流程逻辑三部分分离开来。这使得Web应用的容器内功能测试和单元测试变得困难。StrutsTestCase正是为测试Struts Web应用而创建的Mock测试框架,它无需启动Servlet容器就可以方便地测试.接下来,是创建MemberAction类、MemberForm类、memberForm页和memberList页的时候;然后开始该层测试。

     

    1. 在设计模式中的应用

    在传统的软件工程中,软件开发过程讲究的是前期详尽的需求和系统详细设计,以便开发出软件系统尽量与实际一致,但是这样做有一个缺点,容易造成 " 设计过度"。所谓设计过度,就是在尚未完全理解客户需求的基础上,就根据自己的理解做详细设计,并力求把系统设计得完美灵活。 然而一旦客户不需要那些功能或是改变需求的话, 就会造成开发过程中极大的浪费。

    测试驱动开发 ( test-driven development,TDD)是一种新型的程序开发方式,而不是一种测试方法,它是由 Kent Beck、Devid Astels 等人提出的,与传统的程序设计方法不同的软件开发方式,其基本思想是首先编写测试代码, 由测试来决定要编写哪些程序代码。TDD 的缺点是在前期过少的考虑整个系统架构,过多的强调了先测试后编码的原则,导致后面增加了重构的难度。

    设计模式[4]是在软件设计过程中解决某一类问题的方法。设计模式的基本思想是:根据系统的需求,在经过前人总结得出的方法中选出一种最适合当前系统使用的方法。 使用设计模式的好处是, 它是在无数前人经验的基础上总结得出的一些最本质的设计方法, 使用设计模式可以缩短系统结构设计的时间,能够保证系统的健壮性、扩展性和可维护性。 因此,设计模式是一种指导,在它的指导下,不仅有助于完成任务,而且有助于得到解决问题的最佳办法,从而做出一个优良的设计方案以达到事半功倍的效果。

    为了弥补TDD前期开发对系统结构设计不足的缺点,将TDD与设计模式结合进行软件设计则是一种新型开发方法。

    TDD的基本步骤如下:

    步骤 1 先写一段单元测试代码;
    步骤 2 执行这个测试代码,不能通过编译;
    步骤 3 写所能想到的最简单的程序代码,让单元测试代码可以通过编译;
    步骤 4 再次执行这个单元测试,应该会得到验证失败的结果 ( 如果通过的话,说明单元测试没有击中要害,需要修改测试代码);
    步骤 5 写主程序,让单元测试可以顺利通过验证;
    步骤 6 回到步骤 1,开始写新的单元测试。

     

    软件工程中无数项目的成功经验证明, 设计模式是非常重要和必要的。 对于初步接触

    TDD 的编程人员来说,结合设计模式进行的测试驱动开发最合理的方式应该是:① 花一定的时间做好前期的分析,在研究模式上投入时间;② 在最初以最简单的形式实现模式,以后再增加其复杂性;③ 如果使用了一种模式,而在以后发现无法满足需要时,通过转换的方式将其修改。 使用了结合设计模式进行的 TDD,将会极大的减少修改构架的复杂度,使得修改朝着有序的方向进行。

    结合设计模式的TDD的开发流程基本如下:

    可以看到,设计模式主要是用于业务逻辑层中。 业务逻辑层的作用就是处理系统中的各个业务逻辑,并将所得结果通过接口提供给外部的展现层调用。在开发过程中,遇到需要系统构架调整的时候,只要保持对应用程序的展示层的接口方法不变,无论下面层次中的程序做如何大的改动,前台都不需要做相应的变动,实现了展示层同业务逻辑层相分离的原则。这就是在 TDD 中也要使用设计模式的好处。

     

     

     

    1. 基于MVC的测试驱动开发

    基于 MVC 架构的测试驱动开发过程:由于具有使 View、Controller 与 Model 分离开来的特性,MVC 很适用于 GUI 软件的开发。目前许多 GUI 软件的结构都是基于或者是部分地基于MVC 的。比如,Microsoft Foundation Class(MFC)-----它把 View 和 Controller 集成在视图内,文档负责数据的表示以及存取(Model) ,视图负责显示文档内容(View)以及处理用户界面上的操作( Controller);Struts-----它采用EJB 作为模型,JSP 和 Custom Tag Library 配合作为View,Servlet作为控制器。

    根据对 MVC 架构各层的特点的分析,三层中 Controller 反映了应用程序的功能和流程,并且清楚 Model 和 View 的功能,所以测试驱动开发应该从Controller 出发,首先将开发的重点放在实现程序的功能上,更早地实现需求。由于在 Controller 的开发过程中需要不断地对 Model 和 View 进行重构,为了减少重构的代价,可以将 Model 中未实现的对象用 Mock Object 来代替。当在实现一个用户故事的 Controller 后,或者实现多个 Controller 后,从 Controller 层可以提取出 View 与 Controller 之间数据传递的信息,根据这些信息可以进行View 层的设计和开发,View 的实现主要考虑以怎样的视图来显示这些信息,以及设计怎样的事件来触发Controller中相应的方法。另一方面,当Controller 中可以至少提取出一个完整的 Model 对象时,调用 Model 层代码生成工具生成Model 的程序代码和测试代码。

    测试驱动开发中 MVC架构各部分的关系:MVC 架构的形式化分析,可知 MVC 三层模块之间是相互依赖的关系,而三层中 Controller 层反映了程序的控制逻辑,它的实现依赖于 Model向数据库获取数据,又依赖于 View 将更新的数据显示在界面上。所以在测试驱动开发 Controller 过程中可以设计出 Model 和 View 的接口。View 只是用开发环境提供的各种控件将数据简单的显示在界面上。Model 主要是对数据进行处理,数据处理的过程有其规律性,在绝大部分系统中,这些对象的方法主要是对数据库的操作,包括增加、删除、查询、修改等。三者的对应关系如下图。测试代码传入测试数据对被测代码的 public方法进行测试,随着 Controller 功能的实现,在 Controller 的程序代码部分可以知道,Model 和 View 对象的接口信息逐渐被设计和编写出来。对于 View 来说,它只负责显示图形用户界面,不涉及任何的功能代码,所以 View 层不适合作测试驱动开发,而是将 View 的测试集中在用户界面风格上,比如用户界面的一致性、界面的布局、用户界面之间的切换是否顺畅、颜色的使用是否适当、以及界面的设计是否考虑不同用户的需求等等。在 Web 界面测试方面,配合使用ASPUnit 和 HttpUnit 等自动化测试工具可以提高测试效率。Model 的测试集中在每个对象的方法是否返回正确的结果,Model 的方法确定了,对方法的测试用例也可以确定下来,而测试数据可以从 Controller 测试代码中用于测试的数据中获得。为了提供程序的编写效率,可以先从 Controller 的测试代码和程序代码提取出生成 Model 代码所需的信息,再利用代码生成工具,生成 Model 的测试代码和程序代码。

     

     

    Model信息的提取

    对测试驱动开发 Controller的基本约定

    对 Controller 的开发基本按照测试驱动开发的步骤来进行,将开发的重点放在实现程序的功能上。但是,为了能更好的实现从 Controller 层抽取出 Model和 View 层信息,并保证 Controller 层的可测试性,在开发 Controller 层时,还应该做到以下几点:

    1. 保持 View 的简单性,将事件代码交给 Controller 处理。因为测试 View是困难和繁琐的, 所以View应该是尽可能的简单。View中各控件的event handle事件只是传递的作用,具体的功能较由 Controller 的方法处理。

    2. MVC 三层的通信都要经过 Controller 层。这种限制保证了 Model 和 View 的分离,同时也保证了 Controller 层的测试用例可以完全覆盖整个程序的功能。

    3.为了便于用程序实现在 Controller 中提取自动生成 Model 的信息,Controller 代码中的某些方法和属性的命名应遵循一定的规则,具体如下:

    1) Model 方法的命名:方法类型+Model 类名,如 InsertUser。

    2) Model 几个常用方法的类型为:Insert 表示"插入",Search 表示"查询",Get 表示"单个查询",Update 表示"更新",Delete 表示"删除"。

    3)Controller 测试程序中的一个测试类对应一个 Controller 类,一个测试方法对应一个 Controller 方法的一个测试用例。

     

    1. 嵌入式系统测试驱动开发的策略

    双目标开发策略:对多数嵌入式项目来讲,并行进行硬件和软件开发是个现实。如果只能在目标硬件上运行,有可能会有多个浪费时间的因素。但并不是所有的开发团队都会遇到浪费时间的问题,传统意义上,这也是嵌入式开发者会转而使用评估板来缓解目标硬件的一个原因。评估板是在开发时使用的一种电路板,它有同目标系统相同的处理器配置,理想情况下还有同样的输入输出接口。评估板能保护不会延迟项目,但是这还不够,评估板仍然有构建时间长的问题。双目标开发则是解决上述瓶颈问题的一个策略。双目标是指代码被设计成至少应能在两个平台上运行:代码最终是要在一个嵌入式目标硬件上运行,但它首先在开发系统中写出和测试的,而双目标解决了以下几个问题:它可以在硬件就绪之前就测试代码,并使它在整个软件开发周期里避免硬件带来的瓶颈,还可避免随硬件和软件同时调试带来的互相指责。双目标还会影响设计、对软件与硬件之间边界的关注会产生更模块化的设计。在开发系统中,测试代码会在把代码应用于目标硬件之前来帮助开发人员建立信心,但在双目标方案当中存在其固有的风险。所以这些可能导致在一个环境里运行没有错误的代码却在另一个环境里却测试失败。在嵌入式中的TDD循环可以较好地应对这些问题。

    嵌入式的TDD循环:嵌入式的TDD循环是对TDD微循环的扩展,它可以克服目标硬件所带来的瓶颈。当构建和测试的循环只有几秒钟的情况下TDD效果最好,更长的构建和测试时间会导致采用更大的步伐。对这种快速反馈的需求迫使TDD的微循环脱离目标硬件,而运行在本地的开发系统中。TDD微循环是嵌人式TDD循环的第一个平台,如图所示。

     

     

     

    平台2~4被设计用于缓解用开发平台来运行单元测试所带来的风险。平台5确保完整集成后的系统能够提供可工作的特性。

    平台1:TDD微循环。在这个平台运行得最频繁,通常几分钟一次。大部分代码会在这个平台中写出,并且只在开发系统本地编译。测试是在开发系统里完成的,这样它能给出快速反馈,而不会被硬件的可靠性或可用性的约束拖累。在这个平台中,需要写于平台无关的代码。要寻找把软件和硬件断开的机会,硬件和软件的边界要很清楚,并记录在测试用例中。

    平台2:编译器兼容性检查。要定期的为目标系统作编译,采用为产品而使用的交叉编译器。这个平台是对编译器不兼容的一个早期警告。它会警告移植问题,如头文件不可用,语言支持不兼容,以及语言特性缺失等,不必在每次代码改变时都运行平台2。在每次采用了新的语言特性时做一下目标系统的交叉编译。

    平台3:在评估板上运行单元测试。有一个风险是,编译后的代码在本地开发系统和目标处理器上运行起来是不同的。为缓和这种风险,可以在评估板上运行单元测试。使用评估板可以看到代码在开发系统和目标处理器上行为的差异。拥有在评估板上运行的能力,即使在目标硬件就绪之后可能仍然比较方便。如果有一可疑的目标硬件行为,可以快速地通过在评估板上运行单元测试,以把目标硬件的问题包含进来或排出。

    平台4:在目标硬件上运行单元测试。平台4的目的和平台3相同,只是平台4会使用真实的内存。而且可以运行只能在目标硬件上运行的测试。这些测试可以识别出或者学习到目标硬件的行为。这个平台一个新增的功能是目标硬件上有限的内存。在这种情况下,可以把测试组织成不同的测试套件,使每个套件都能装进内存中。

    平台5:在目标硬件上运行验收测试。最后,需要在目标硬件上运行自动化的和手工的验收测试来保证产品特性。这里要确保任何不能完全被自动化测试的、依赖于硬件的代码都会被手工测试。

     

     

    [1] Badreddin O, Forward A, Lethbridge T C. A test-driven approach for developing software languages[C]// International Conference on Model-Driven Engineering and Software Development. IEEE, 2014:225-234.

    [2] 苏庆.SU Qing 测试驱动开发在Java语言课程实践中的应用[期刊论文]-广东工业大学学报(社会科学版) 2008(z1)

    [3] 程烨.高建华.CHENG Ye.GAO Jian-hua与设计模式相结合的测试驱动开发方法[期刊论文]-计算机工程与设计 2006(16)

    [4]Pipka J U. Test-Driven Web Application Development in Java[C]// Revised Papers from the International Conference NetObjectDays on Objects, Components, Architectures, Services, and Applications for a Networked World. Springer-Verlag, 2002:378-393.

    [5] Kent Beck. Test-Driven Development—By Example[J]. Pearson Schweiz Ag, 2003.

    [6] 黎利 基于MVC的测试驱动开发研究[学位论文]硕士 2007

    [7] 陈立群.CHEN Li-qun 测试驱动开发在J2EE项目中的全程实践[期刊论文]-计算机工程与科学2008(4)

    [8] 张扬.黄厚宽.ZHANG Yang.HUANG Hou-kuan 测试驱动开发及开发实践[期刊论文]-计算机技术与发展 2006(5)

    [9] 齐山松.姬进.QI Shansong.JI Jin测试驱动的嵌入式开发与实践[期刊论文]-电子科技 2013(8)

     

     

     

    展开全文
  • tdd(测试驱动开发)的概述

    千次阅读 2019-06-23 10:52:35
    简单介绍tdd(测试驱动开发)的方法,思想与一些best practices

    最近的工作的项目,使用了tdd(test-driven development测试驱动开发)的开发模式。
    这两几年大概听说了无数种xxx-dd, ddd, tdd, atdd, bdd, fdd, udd各种名词眼花缭乱,当然很多dd其实也有相互借鉴(抄袭)的部分。不过仔细想来测试驱动应该也是很久以前就有的一种技术了,21世纪早已经是测试自动化的年代。tdd讲师还推荐了有一本书叫《XUnit Test Patterns: Refactoring Test Code》。书早在2007年就出版了,自己也算是后知后觉。

    接触tdd之前的想法。

    代码只要写的漂亮就行了。代码写得漂亮了,看代码就全懂了,文档的什么都是垃圾!
    代码写得好,可读性强,造成问题的概率就会低。
    程序设计是很重要的,如果不做设计就开写,结果就是产生一坨垃圾!所以不要急着去写代码,写做好设计,理清思路后再动手!
    突然有一天以为tdd教洗脑能手出现,提出了令人毁三观的观点----不要在写代码之前去做设计!
    什么?!为什么会这样?!
    展开之前,先简单介绍一下tdd的方法论

    tdd的步骤

    tdd

    1. 先写测试代码,并执行,得到失败结果

    比如我们要写实现一个功能,当输入值大于等于0时返回true,当输入值小于0时返回false。
    按照tdd的步骤先要写一个肯定会失败的测试,注意测试代码先于实现代码,因为还没有实现代码,所以测试必定是失败的。
    大概是下面这种样子。

    • 测试代码
    @Test
    void greaterEqualThan0Test{
      // given 
      int input = 2;
      // when
      boolean result = greaterEqualThan0(input);
      // then
      assertThat(result, equalTo(true));
    }
    
    • 实现代码(未完成)
    public boolean greaterEqualThan0(int input){
      return false;
    }
    

    运行测试,得到红灯。

    2. 写实现代码让测试通过

    先不管任何设计啥的,只要让测试过了就行,随便乱写。比如下面的的代码。

    • 实现代码
    public boolean greaterEqualThan0(int input){
      return true;  // 我勒个*这都可以!?
    }
    

    运行测试,得到绿灯。

    3. 重构代码,并保证测试通过。

    实现太简单。没什么能重构的,跳过。

    4. 反复实行这个步骤 测试失败 -> 测试成功 -> 重构

    再写一个测试

    @Test
    void greaterEqualThan0Test_2{
      // given 
      int input = -1;
      // when
      boolean result = greaterEqualThan0(input);
      // then
      assertThat(result, equalTo(false));
    }
    

    执行测试,失败。
    写实现。

    • 实现代码(重构前)
    public boolean greaterEqualThan0(int input){
      if(input >= 0){
        return true;
      }
      return false;
    }
    

    重构
    因为有了自动测试的保护,你可以尽情地修改之前看得不爽的代码,为所欲为。
    其结果可能是下面的情况。

    • 实现代码(重构后)
    public boolean greaterEqualThan0(int input){
      return intput >= 0;
    }
    

    当然我们还可以把测试代码也重构一下。

    • 测试代码
    @ParameterizedTest
    @CsvSource({"2, true",
                "-1, false"
      })
    void greaterEqualThan0Test(int input, boolean expectedResult){
      // given 
    
      // when
      boolean result = greaterEqualThan0(input);
      // then
      assertThat(result, equalTo(expectedResult));
    }
    

    之后也可以多加case

    • 测试代码
    @ParameterizedTest
    @CsvSource({"2,  true",
                "-1, false",
                "0,  true"
      })
    void greaterEqualThan0Test(int input, boolean expectedResult){
      // given 
    
      // when
      boolean result = greaterEqualThan0(input);
      // then
      assertThat(result, equalTo(expectedResult));
    }
    

    这种小学生都能完成的代码,可能给人杀鸡焉用牛刀的感觉。不过相信能给大家一种比较直观的印象。
    需要注意一点,就是在第一步写测试代码的时候,一定要运行测试,确认测试是失败的。我觉得有两个目的。

    • 思考你应该如何验证你的程序,帮助你写出一种比较容易验证的代码。
    • 防止测试本身是错误的。
      比如,你可能一不小心手滑写了下面一个测试。
    // given 
    int input = 2;
    // when
    boolean result = greaterEqualThan0(input);
    // then
    assertThat(true, equalTo(true));
    

    这个测试是没有任何意义的,对你的代码重构起不到任何保护的作用。

    另外请注意!本人在写实现大码之前没有做设计!!!,虽然很有造作的嫌疑。。。

    tdd的思想(个人观点)

    首先声明一下这些只代表本人的观点,主要来自于工作与上一些tdd教徒的交流体会。我不是很确定tdd是否有这样系统的原理。

    1. 测试代码先于实现代码,不允许有没有测试代码的实现代码。

    这个字面意思还是很好理解。在这样的开发方式中,你所开发的功能绝对会被测试保护的。
    如下图。
    在这里插入图片描述
    而下面这种情况是tdd所希望避免的。实现的代码的范围超出了测试代码。而超出的部分没有受到测试的保护,是容易产生bug的部分。
    在这里插入图片描述
    有一些开发者可能喜欢先写代码,后写测试。从tdd的角度来说,这样何容易造成上图的结果。因为当你的实现代码变复杂了之后,你可能遗漏一些测试的case。
    另外,先开发后测试的另一个缺点是写测试的难度会变高。这应该也很好理解,当你设计程序时,如果你没有考虑可测试性的要素,很可能最终的代码会变得超难测。比如一个没有返回值,但有副作用的函数,写测试就会比较麻烦。(当然我不是说写函数一定要有返回值 (´ε`;))

    2. 好的设计是不断的重构中实现的,想要在写代码前就作出好的设计是很有难度的。与其在最初去做好的设计,不如不断的重构,使代码进化。

    其实这个思想应该是Emergent Design。本身和tdd并无直接关系。但由于培训师讲师非常提倡这个思想,结果不慎被洗脑,在这里讲一下。
    这个思想的缘由大致是,程序用来实现具体的业务逻辑,当这也业务逻辑是复杂的情况下,理解业务逻辑是需要花费时间的。
    当程序员开始开发,处于程序设计阶段时,对业务地理解是相对少和浅的。对业务的理解是随着开发的推进而增加。
    在这里插入图片描述
    这种情况特别在维护现有产品时更容易显现。(一个原本已经能够运行的成品,因为一些功能的修改或增加,突然一些地方就跑不了了。。。仔细一查,发现了一些不为人知的业物逻辑。。。orz)
    当你在对业务的理解相对少的前期阶段,做出好的设计是很困难的。而在开发有一定进展后,对业务有了更好的理解,这时候做出的设计就会更符合需求更加合理。
    另外ttd(也不一定源于tdd)把产品品质分成两种。

    1. 外部品质: 保证程序根据需求运行,不出错
    2. 内部品质: 产品的设计,可维护性等
      在这里插入图片描述
      外部品质是通过测试来保证。而内部品质则是通过重构来实现更好的设计。、

    3. 不做过度的设计(over engineering)

    这个观点其实和Emergent Design也是关联的。程序开发的前期阶段为了程序更好的扩展性,往往做了一些超越目前需求的设计。貌似是有数据支持,不过因为本人太懒,懒得找。维基百科上说是,这些为了扩展性所做的设计,最终得到使用的不足10%,所以90%的时间都会被浪费。
    另外具有扩展型的设计,因为这些设计的部分很可能现阶段没有被使用,反而会迷惑其他的开发者。(比如你设计了一张数据表,然后为了将来不对数据表做更改,加了一列 colum_for_future_extension)
    对此还有一个标语专门提倡这种观点。YAGNI。You ain’t gonna need it!
    所以tdd所期望的是程序的实现能与现阶段的需求完全契合,不做过度的设计。你要做的是根据现在的需求写测试,让测试通过,并此需求上把代码设计的更好更合理。因为测试为先,测试又根据需求来写。这样的话会比较难产生over engineering。

    一些推荐的实践方法(best practices)

    根据上面介绍的思想,tdd还有有一些推荐的实践方法。

    1. baby step

    tdd希望程序员开发时,频繁地实行这个开发循环。
    tests failed -> tests passed -> refactoring
    不要写了半小时测试,然后运行一下,然后又开发半小时。
    频繁地运行测试,来验证自己的实现或者对业务的理解是否正确。步步为营,循序渐进。如果测试出错了,回退到之前测试通过的状态也比较容易。

    2. 不同时做两件事情

    tdd的开发步骤分为,tests failed, tests passed, refactoring,每个步骤只做这个步骤该做的事情。
    比如在test passed的步骤里,你要做的就是让测试尽快通过,而不是一边想着让测试通过,一遍还要重构。结果在测试还没通过的情况下就先重构,结果就是那个步骤出了错就比较难判断。
    类似地,在写失败测试时,不要就急着把业务逻辑给实现了。

    3. 通过发现code smell来改进代码

    好的代码是很难描述的,而且众说纷纭。不过什么代码是不好的,倒是有一定共识,并且容易具体化的东西。
    比如一个方法写得太长了,很有可能需要把这个方法拆分成几个抽象度更高的方法。
    不好的代码有一个专门的术语,code smell(代码发臭了。。。)。在此引用一下维基百科。
    code smell
    在tdd的第二步让测试通过后,第三步refactoring,code smell便能帮助你发现需要refactoring的地方。

    结语

    刚受到tdd教洗脑时,感觉这种理论根本就是毁灭性的,反智的!有点像达尔文的《进化论》。《进化论》之前人们普遍接受神创思想。多样与复杂的生物,只有神能够来创造。而《进化论》认为,不需要神的帮助,生物也能自主从简单演化到复杂,从单细胞演到多细胞。如果很极端地去遵循tdd的教义(我的例子中其实也没有严格遵循啦 ^ ^ ),那在编程时不用作任何设计,不断通过重构来演化,最终得到的会是高品质的程序。
    不过我最终还是入教了。。。那么tdd就是是不是个神器?之后的文章想谈谈在实践tdd时遇到的问题与感想。

    展开全文
  • Python测试驱动开发(TDD)

    千次阅读 2018-08-16 22:56:02
    Python测试驱动开发(TDD) 前言:TDD是一种敏捷开发模式,而不是测试方法。 目录 Python测试驱动开发(TDD) 目录 单元测试与功能测试的区别 “单元测试/编写代码“循环 遵守不测试常量规则 有用的TDD概念 ...
  • 【编者按】测试驱动开发(TDD)始于上世纪 90 年代,时至今时今日,依然只有少数的开发者在践行着。本文作者从软件开发者的角度,又一次帮助我们定义了测试驱动开发,解答了众多开发着对 TD...
  • 测试驱动开发的一些理解

    千次阅读 2017-08-02 23:30:29
    测试驱动开发的一些理解测试驱动开发解决什么问题? 系统测试和集成测试不容易覆盖一些代码细节,难以做到很高的代码覆盖率;测试驱动开发编写的测试一般是单元测试,而且由开发者编写,针对单个模块容易做到各个...
  • 分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!...在JUnit的作者Kent Beck的大作《测试驱动开发:实战与模式解析》(Test-Driven Development: by Example)一书中有这么一段内容:...
  • 谈谈对测试驱动开发思想的体会

    千次阅读 2017-11-11 13:44:05
    最近学习了一本书《Python Web开发:测试驱动方法》,贯穿全书的便是测试驱动开发的编程思想。有点儿兵马未动,粮草先行的兵家思想。先简单总结一下这本书带给我的收获:1.学习了测试驱动开发的一种编程思想,与传统...
  • Java测试驱动开发(TDD)一、基础知识1.什么是TDD1.1怎么开始TDD 一、基础知识 1.什么是TDD TDD要求你先编写测试,再编码实现代码。 1.1怎么开始TDD 编写测试代码 运行测试代码 编写实现代码 运行测试代码 重构 ...
  • Android测试驱动开发实践2

    千次阅读 2013-10-15 11:43:59
    在Android应用开发中,采用测试驱动开发方法,可以同时兼顾开发效率和软件质量,避免片面强调速度而使软件质量下降的问题,同时采用TDD所倡导的测试-实现-重构流程,可以使系统不停进化,最终达到目的。但是在...
  • 测试驱动开发的艺术.pdf

    千次下载 热门讨论 2020-07-30 23:32:40
    测试驱动开发的艺术》介绍了一种更快更好的软件开发方法——测试驱动开发。全书共分三部分:第一部分讲述了TDD和ATDD的相关知识、基本概念、方法,为测试驱动开发打下基础;第二部分将测试驱动开发用于具体的实践...
  • 测试驱动开发的好处

    千次阅读 2014-08-19 15:06:59
    测试驱动开发TDD的好处: 1. 快速反馈 2. 有利于解偶,不好测试的用例需要重构 3. 测试用例是重构的基础, 4. 先定义好API接口,基于接口编写测试用例和实现,目标明确 5. 一个个测试用例,就是“摸...
  • 测试驱动开发优缺点

    千次阅读 2015-06-30 19:16:03
     近期学习了TDD(测试驱动开发),但是由于没有亲身使用太多,所以说不出太多感受,但是看到了一篇博客,觉得讲的挺好的,跟大家分享下。 正题  不觉间,采用测试驱动开发(Test Driven Development)半年有余,...
  • 测试驱动开发初体验

    千次阅读 2019-01-14 17:42:01
    测试驱动开发有下列三定律:  编写不能通过的单元测试前,不可以编写生产代码。  只可以编写刚好无法通过的单元测试,不能编译也算不通过。  只可以编写刚好足以通过当前失败测试的生产代码。 个人是不喜欢...
  • TDD测试驱动开发过程

    2019-08-05 16:37:17
    TDD测试驱动开发过程具有4 个基本步骤: 1)编写测试 2)核对和确认测试 3)编写产品代码,接着测试 4)重构产品代码。 其中一个例子可为,用户必须记录产品的生存期值。一项完善的测试需要确保用户数据输入是...
  • 测试驱动开发(TDD)开发思路

    千次阅读 2018-07-22 10:00:57
    前端时间看到一个开发思路,就是测试驱动开发(TDD),试用了一段时间感觉还不错,安利一下。当然可能很多人已经知道了,跳过就好 测试驱动开发,就是我们在开发代码之前,先去做好测试用例,进行测试发现少了什么...
  • 最近看了下测试驱动开发(Test-Driven Development), 第一部分用一个资金实例讲解测试驱动开发模式(采用java编写示例),感觉挺不错的。  不过这种模式需要编写跟功能代码量相当的测试代码;一步一步的测试保证,...
1 2 3 4 5 ... 20
收藏数 293,933
精华内容 117,573
关键字:

测试驱动开发