这是鼎叔的第六十四篇原创文章。行业大牛和刚毕业的小白,都可以进来聊聊。

欢迎关注本公众号《敏捷测试转型》,星标收藏,大量原创思考文章陆续推出。

本文观点参考自 Lasse Koskela,他是《测试驱动开发的艺术》的作者。

软件缺陷通常是由低质量的代码引起的,但是在复杂项目中,要维护这些代码简直就是噩梦。新加入的开发者想对它进一步修改,更是举步维艰。测试驱动开发,或许能解决这个问题,利用测试构建出高维护性和满足客户需求的软件,它也是 XP(极限编程)的核心实践。

我们常说的TDD,通常指细节层面的UTDD(单元测试驱动开发),以测试驱动的方式编写开发。行业还有一个概念- ATDD(验收测试驱动开发),指在较高层次(特性功能层),以测试驱动的方式构建系统。前者保证内部质量,后者保证可见的外部质量。

TDD 带来高质量和高效率

提倡短周期的 TDD 从一开始就能保证较高的代码质量,它完全颠倒了软件开发的旧方式(即先设计,然后编码实现,再测试),即先写测试描述目标,然后写代码达成目标,最后重构改进设计。

完全可测试的代码和不断演化的简洁设计,能避免开发陷入代码越写越糟的恶性循环。TDD 保证了所有代码都是可用,且被测试覆盖的。由于不用实现考虑实现细节,我们可以从更健壮的角度思考函数接口行为和外部调用方式。

写代码要基于测试先行的小步前进,增量式构建系统,在发生错误时很容易定位代码行,这样可以大幅节约后期的调试时间。我们也就有时间做更有意义的事,如清理代码,学习工具及技术。

而 ATDD 则是把客户需求全部转换成可执行的具体功能测试,拉近了客户和开发者的距离。在 ATDD 中我们更关注系统行为的测试,而非对象行为的测试。

TDD 和 ATDD 非常互补,ATTD 能驱动开发过程(做正确的事),而每个功能点上则使用 TDD(正确地做事)。它们的组合让我们对交付的代码更有信心。

TDD 三步曲(测试 - 编码 - 重构):正确地做事

三步曲代表着红(测试失败)- 绿(测试通过)- 重构代码的状态迁移。

写恰好足够的代码,仅仅是为了修复当前失败了的测试,不要一下子写出整个功能的代码再来花很长时间让测试通过。

所谓增量式开发(小步快跑),就是在 “实现新功能” 和 “调整设计” 两件事中来回切换,采用更经济的演进式设计方法。我们需要为可能的变化进行准备,但又要避免做无用功。最后的重构则是 “在不改变外在软件行为的基础上,改进程序的内部结构”。

TDD 整个过程要遵守严格的纪律,每次修改后运行自动化回归测试,用 “测试 PASS 的信心” 来交换开发速度。

ATDD-做正确的事

增量式开发模式中,客户有权决定哪些功能优先开发,从而被激发对项目的热情,而验收测试(AT- Acceptance Testing)则是整个团队(开发,测试,产品,客户)沟通的共同语言。“以需求文档为沟通媒介” 很难清晰地表达出意图,而 “以测试为规约” 则更加精准、可靠和直接,缩短反馈周期。

测试驱动开发的工具支持

针对单元测试级别的 TDD 工具统称为 xUnit。而 ATDD 的测试框架类型就更多了。前些年软件公司使用的最热门的验收测试工具是 Fit/Fitness,表格形式的工具可以让非技术背景的客户和产品经理一起参与测试。Fit 表格关联了测试夹具,可以自动执行表格内容的测试,并显示对用户友好的、多彩的测试结果。

此外还有纯文本的测试工具,比如通过关键字驱动或者利用日志来测试。

持续集成基础设施也是至关重要的保障,因为采用 TDD 的团队会共享代码,任何人都可以修改代码。

静态代码分析工具和代码覆盖率分析工具,对刚采用 TDD 的团队可以具体指出代码测试的不足,这个帮助很有必要。

TDD 实践步骤详解

第一步:从需求到测试

开发者首先要把需求划分为要做的事(任务),用测试的形式来表达任务有利于我们记住要 “完成” 的定义,避免脱离了用户需求。一个好的 “测试” 应该是原子化的,独立的。

我们如何为一段还不存在的代码写测试呢?这需要我们想象产品代码应该如何易用,这种想象可以称之为 “意图编程”。我们把注意力集中在 “能有” 的,而不是 “已经有” 的东西上,把需求分解为一系列小的,紧凑的测试。

第二步:用 TDD 开发模版引擎

第一步得到的测试列表是一个活文档,可以根据我们的进展而不断添加,接下来要让它们挨个通过。用自己认为合理的方式设计模板引擎的工作方式,把模板文本作为参数传给构造函数,验证结果与期望是否一致。

当然,编译器一开始会报错,因为某些类根本不存在,我们添加类,继续补充相应的方法。然后运行测试,测试必然会失败(因为我们还没有实现这些方法),继续补充最基本的产品代码,直到第一个测试通过,这个过程要尽可能简单快捷。

对于复杂逻辑的实现,我们可以采用广度优先或者深度优先的方式。如果是广度优先,我们会集中实现高层的功能,低层功能暂时用伪实现。若采用深度优先,我们会先实现底层功能,在所有底层功能都实现后才会组合来实现高层功能。

第三步,清除伪实现(尤其是硬编码),重构代码。

验证添加的测试确实被执行了,测试列表都通过了(包括特殊情况的数据测试),我们通过重构避免代码的 “腐坏”,清理重复和冗余的代码,移除多余的测试。利用夹具使测试更加紧凑,用测试替身替代真实对象。

第四步,添加错误处理,最后让代码尽量精简。注意验证异常中的详细信息,保持方法中的代码抽象层次的一致性,这样会提高代码的可读性。

第五步,增加更多的系统测试,如耗时的性能测试。测试替身可以提高测试速度,降低依赖性。

TDD 的指导原则

总之就是:绝不跳过重构,尽快变绿,犯错后减慢速度。

为了提高可测试性,设计上要尽量使用组合而非继承,掌握参数化测试手段(数据驱动测试),正确恰当地隔离依赖。

如果我们是在糟糕的遗留代码上进行 TDD,首先要进行代码分析,确定变更点代码,进而确定测试点,从近距离测试,也从远距离寻找合理的测试点(如网络和日志),小心地移除某些依赖,并暴露依赖的接缝。一旦有了足够的测试覆盖,就可以放心地引入变更。

集成测试中的 TDD

TDD 并非只对应单元测试,集成测试也是用来做 TDD 的。和单元测试的不同在于,集成测试会真实地访问数据库,访问文件系统,花费的时间更多,需要更完整的基础设施,可能需要改变数据库模式,进行针对性的重构。不足之处是集成测试难以模拟特定的异常场景。建议我们充分利用单元测试和集成测试两者的优点来实践 TDD。

多线程并发是 TDD 实践的难点,代码中的任何同步都会对相邻线程的并发性产生影响,因此并行编程出错的可能性更大,还可能遇到 “死锁” 和 “饥饿” 等现象。我们需要针对 “线程安全” 进行编码,在测试中尽量避免用 “钩子” 来控制产品代码的执行过程,以及等待验证异步调用的结果。

ATDD 的进一步阐述

验收测试是用业务问题领域的语言来描述的测试用例,描述简洁准确,无歧义,侧重于 “做什么” 和原因,而非 “如何做”。最终的所有权属于客户(利益相关方)。测试角色在团队中既属于领域专家,也属于技术专家。

验收测试的格式,可能是声明式的表格结构,因此验收测试自动化工具可能和系统实现的语言不同,强调客户容易理解,简单易懂。ATDD 要在功能层面保证 “软件做了我想要的事情”,而不是从技术上保证。

一个 ATDD 过程周期非常简单,分为:挑选一个用户故事(从需求中拆解)、写测试用例、自动化测试、实现功能,这四大步骤

建议一步步实现验收测试,而不是一下都实现。大部分团队都会自己实现验收测试,不依赖于专门的验收测试人员。

注意,实现功能这个 “第四步”,就可以扩展为一个或一系列的 TDD 小周期(测试 - 编码 - 重构),这样 ATDD 和 TDD 就在不同层次上形成紧密协作的闭环,相辅相成。

ATDD 给出了需求完成的 “定义”,通过有意义的例子,而不是复杂模糊的描述来表达需求。每个人都会贡献自己特有的知识和技能来解决问题。客户能看到自己的需求被真正满足了;开发人员看到客户参与验收测试,并认可了自己代码的价值。

验收测试是否应该操作真实用户的外部界面?答案是 “看情况”,如果真实界面难以访问,或者其反馈成本高、性能慢,我们也可以绕过界面,通过 API 或者内存数据库来进行验收测试。但是在这么做之前,我们先要确认替换后的被测系统和替换之前是否足够相近,或者不得不这么做。

验收测试不必测试所有东西,而是聚焦用户故事的本质特质,同时避免波动频繁带来的维护性问题,选择技术障碍最小的地方越过它。

实现 ATDD 的方式主要有:

1 端到端。理想情况下,应该把被测系统当成一个整体,端到端的视角,和客户观察系统的角度一样,最能体现系统的真实情况,以及对系统的广泛覆盖。但是这种测试太脆弱,尤其 UI 变更频繁,同时速度也慢。

2 绕过 UI 的测试,即绕过系统的壳,避免了不必要的改动,调用抽象 UI 或者 API 来访问系统内部。这样易于实现且执行速度快,但会让客户困惑,也需要手工测试弥补图形界面的测试质量。

3 直接测试内部逻辑。利用验收测试工具,把业务逻辑隔离再几个精准的测试中。它和单元测试的不同在于,前者是用客户领域的语言编写的,后者是为开发者编写的。

写在最后,在 ATDD 技术实践上可以用到的技巧还包括:

1 把系统的一些非关键构件替换成测试桩或者仿真器,利用测试后门(替代性接口)等。

2 加快测试执行速度。比如检查所有的测试是否真的需要这么多的初始化,以及用一次初始化完成一批用例的前置准备。

比如把测试套件分为两组,一组是有副作用的(如写数据库),一组是没有副作用的,先执行后者。

还有,减少磁盘 I/O 访问的动作,或者分布式执行任务,利用好负载均衡。

3 减少测试的复杂度。如消除代码重复,优化命名,利用公共函数把验收测试组织成有机的整体,利用缓存环境对象。

4 管理好测试数据。在保持好自动化验收测试的代码干净整洁的同时,提高测试数据的可管理性。如把测试数据小块化,或动态产生测试数据,以及对测试数据做好版本控制。


↙↙↙阅读原文可查看相关链接,并与作者交流