测试驱动开发
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。
Kent Beck最早在其极限编程(XP)方法论中,向大家推荐“测试驱动”这一最佳实践,还专门撰写了《测试驱动开发》一书,详细说明如何实现。经过几年的迅猛发展,测试驱动开发已经成长为一门独立的软件开发技术,其名气甚至盖过了极限编程。
测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码,然后只编写使测试通过的功能代码,从而以测试来驱动整个开发过程的进行。这有助于编写简洁可用和高质量的代码,有很高的灵活性和健壮性,能快速响应变化,并加速开发过程。
测试驱动开发的基本过程如下:
① 快速新增一个测试
② 运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过
③ 做一些小小的改动,尽快地让测试程序可运行,为此可以在程序中使用一些不合情理的方法
④ 运行所有的测试,并且全部通过
⑤ 重构代码,以消除重复设计,优化设计结构
简单来说,就是不可运行/可运行/重构——这正是测试驱动开发的口号。
测试驱动开发的优势
1) TDD根据客户需求编写测试用例,对功能的过程和接口都进行了设计,而且这种从使用者角度对代码进行的设计通常更符合后期开发的需求。因为关注用户反馈,可以及时响应需求变更,同时因为从使用者角度出发的简单设计,也可以更快地适应变化。
2) 出于易测试和测试独立性的要求,将促使我们实现松耦合的设计,并更多地依赖于接口而非具体的类,提高系统的可扩展性和抗变性。而且TDD明显地缩短了设计决策的反馈循环,使我们几秒或几分钟之内就能获得反馈。
3) 将测试工作提到编码之前,并频繁地运行所有测试,可以尽量地避免和尽早地发现错误,极大地降低了后续测试及修复的成本,提高了代码的质量。在测试的保护下,不断重构代码,以消除重复设计,优化设计结构,提高了代码的重用性,从而提高了软件产品的质量。
4) TDD提供了持续的回归测试,使我们拥有重构的勇气,因为代码的改动导致系统其他部分产生任何异常,测试都会立刻通知我们。完整的测试会帮助我们持续地跟踪整个系统的状态,因此我们就不需要担心会产生什么不可预知的副作用了。
5) TDD所产生的单元测试代码就是最完美的开发者文档,它们展示了所有的API该如何使用以及是如何运作的,而且它们与工作代码保持同步,永远是最新的。
6) TDD可以减轻压力、降低忧虑、提高我们对代码的信心、使我们拥有重构的勇气,这些都是快乐工作的重要前提。
7)快速的提高了开发效率
基本测试阶段
单元测试又称模块测试,是针对软件设计的最小单位─程序模块,进行正确性检验的测试工作。其目的在于发现各模块内部可能存在的各种差错。单元测试需要从程序的内部结构出发设计测试用例。多个模块可以平行地独立进行单元测试。
通常,在单元测试的基础上,需要将所有模块按照设计要求组装成为系统。这时需要考虑的问题是:
在把各个模块连接起来的时侯,穿越模块接口的数据是否会丢失;
全局数据结构是否有问题;
单个模块的误差累积起来,是否会放大,从而达到不能接受的程度。
在单元测试的同时可进行集成测试,发现并排除在模块连接中可能出现的问题,最终构成要求的软件系统。子系统的集成测试特别称为部件测试,它所做的工作是要找出集成后的子系统与系统需求规格说明之间的不一致。
系统测试,是将通过确认测试的软件,作为整个基于计算机系统的一个元素,与计算机硬件、外设、某些支持软件、数据和人员等其它系统元素结合在一起,在实际运行环境下,对计算机系统进行一系列的组装测试和确认测试。系统测试的目的在于通过与系统的需求定义作比较,发现软件与系统的定义不符合或与之矛盾的地方。
好的测试具有的特点
1)自动性(Automatic)
执行测试。执行测试应该是足够简单的,这样,我们就可以随时随地的进行测试。
检查测试结果。测试必须能够自行判断成功还是失败。
2)完备性(Thorough)
良好的测试应该是彻底的,所有可能会出错的地方都应该测到。
3)专业性(Professional)
测试代码的书写和维护应该保持和生产代码一样的专业水准。产品代码中必须遵循的常用的设计准则,如:数据封装、DRY原则、高内聚、低耦合等等,在测试代码中也一样要遵守。
4)可重复性(Repeatable)
测试用例之间是独立的同时,也应该是独立于环境的。目标就是保持每个测试能够以任意顺序、不断的重复执行,且得到同样的结果。这意味着测试不能依赖于不可控的任何外部环境。使用Mock对象来隔离测试,保证测试不依赖于外部环境。
5)独立性(Independent)
测试代码必须保持精简、整洁,这就要求测试是专注的、与环境和其他测试隔离的。
现在越来越多的项目在开发时采用TDD,其实不是有了TDD就不需要测试Team了,采用TDD一则使开发Team对自己的代码在修改重构时更有信心,二则也将功能保证更多的交于开发本身,减轻测试Team的工作量。这样测试team就有时间来做些更高级别的测试,如压力测试,性能测试,甚至web自动化测试等。
在单元测试时,经常会有人问测试覆盖率,我在实践过程中没有追求过这个数据,做过统计,我项目中的测试覆盖率不超过30%,只有在比较复杂或者业务逻辑模块才会有大量的测试,在我们传统的action-service-dao开发模式下,业务逻辑主要集中在service层,开发的时候我一般也是从service层入手,从中间向两边开发,一边是Dao,一边是action、页面。所以我的单元测试主要是针对service层写的,主要测试目的有两个:测试业务逻辑是否正常、测试sql执行是否正常。对于service层比较复杂的,我也会把对dao的测试和service业务逻辑测试分开。页面测试只能启动服务器在浏览器里手工测试了,没办法。随着项目中测试的增多,业务的正确于否我已经更多通过单元来保证,已经很少启动服务器了,真是节省了不少时间。
之前在InfoQ有一个团队介绍了他们项目的测试策略,他们主要使用下面三种测试:
• 系统测试
• 皮下测试
• 单元测试
它们之间的区别主要在于被测试内容的范围。系统测试指的是通过应用的外部接口进行运作,无论对象是一个浏览器,文件下拉菜单,队列,window窗体应用或者其他的什么东西。
皮下测试运行在外部用户接口之下。如果测试的是Web应用,皮下测试就是指在真实的类以及环境部署到位的情况下,通过命令处理器进行发送的表单对象。绕过UI层逻辑,直接到达domain层。发送表单对象,抛出”成功/失败”的执行结果。
对于最底层而言,使用单元测试。单元测试用于测试一个类,并且可以是fast test 或者 slow test中的一种。
单元:皮下:系统测试在测试工作中各自占的比重差不多是 10:2:1。
单元测试在严密的TDD模式下开发。在功能实现之前编写单元测试,并用这些测试驱动代码设计。这些测试可以帮助发现设计问题、封装问题、代码异味等。
皮下测试,顾名思义,所有的测试都是在用户界面之下进行的。在MVC应用程序中,皮下测试是测试控制器下面的所有内容。对于Web service,一切测试都在终端下进行。皮下测试的思想是,应用程序的最上层不执行任何实际的业务逻辑,而只是外部接口与底层服务之间的连接。皮下测试的重要性体现在希望在抛开如用户接口和外部服务这类外部连接点的情况下,能够在整个系统运行的同时测试业务逻辑。相对于单元测试关注小模块的设计,皮下测试关注的不涉及设计,而是测试整个系统的基本输入和输出。由于皮下测试不是基于设计而是基于高级(业务)行为,它们是理想的基于场景的测试策略,如BDD或Testcase Class per Fixture模式。如果项目要进行大的重构,就需要这些高层次的测试,为商业行为建立全面的安全保障。由于皮下测试更关注于端对端的逻辑,所以它也是标志功能点完成的一个重要的目标点。
有时候也把全系统测试称为“UI 测试”,当然它不只来自浏览器,还来自 REST 端点、FTP 或批处理文件,UI 测试只是全系统测试的一个子集。全系统测试背后的思想是,按照软件在生产环境中的使用方式来测试它们。对于一个 MVC 应用程序来说,就是基于浏览器的测试。对于批处理文件来说,我们会使用实际的文件。对于 REST 使用实际的 HTTP 请求。对于消息则使用实际的队列和消息。通过此测试来验证应用程序在部署到生产环境之前是否能按照预期工作,很多项目都是手工测试,当然通过web自动化测试也是个不错的选择。