对已有系统如何开展 TDD
前言
最近接手一个已经上线运行的产品,并负责后期的开发和维护。想着正好用这个过程尝试如何对已有产品进行 TDD 工程实践的可行性。今天就分享一下在这个过程中的感受和思考。Kent Beck 在他的经典《测试驱动开发》一书中提过,为已经能够工作的代码编写测试将是很难的一件事情,因为:
你的代码不是按照可测的标准进行编写。很难编写测试。
没有测试的反馈,后续改动、重构无法第一时间让你知道,例如哪些地方曾经好用而现在出现了问题。
如果你要先写测试,再修改功能代码,将会持续很久。因为你会遇到上面 1 和 2 的困境。他们是一个死循环。
Kent 给的建议是对于已经 work 的代码暂时不要去动它,获得反馈也并不是只能通过测试代码。也可以从同事那里通过结对编程来获得。总之先考虑如何打破上面的死循环。开启一小步才是重点。
现状
我最近接手的产品是一个基于 RESTFull 的 API 服务,给其他产品提供需要的数据和操作。当然不出意外,系统没有 function level 的 Unit Test。但是好的是有少量的基于 Postman 的 Http Request 脚本。也有开发人员为了开发功能编写的调用 API 的小程序。每次产品 Release 的实际测试,都是 QA 用一个使用当前 API 最多的另一个业务线产品做回归测试,从而知道最近的改动是否一切正常和达到预期。
Todo List
对方是 3 个开发人员的团队,我只有一个人。现实情况就是:1 个月的交接期之后,我既要保证当前产品的稳定,又要保证后续开发和改动的进度。于是我给自己如下几个问题来思考下一步的行动方案:
如何快速熟悉并掌握系统?传统的方式无外乎:看文档;看代码;跟代码。如果没有文档,代码注释不够,那一般只能最后一步运行程序设定断点,跟代码了。如果代码量还不少,从何下手也是一个问题,时间有限,总不能漫无目的的跟踪吧?基于这些考虑,我打算基于仅有的几次和开发人员的交接的会议中整理出系统的主要业务逻辑范围。基于这些业务逻辑形成验证目标场景,也就是对于系统确定的输入可以有明确的期待输出。我可以将主要业务逻辑在系统集成这个层面编写测试代码。注意:这里不是 Function level,而是 System level。但是目的是通过这些验证作为抓手成为跟踪代码的入口点。因为需要有一些断言来验证实际反馈和预期结果是否一致性。所以我仍然借助于单元测试框架,这里我使用的是 xunit。
如何保证添加新功能/修改功能后系统依旧稳定?当我按照上面的过程将主要的业务逻辑部分都写上至少一条验证 Case 之后。基于学习代码的经验,我会有更多逻辑的认识。这会驱动我将这些新发现的逻辑也用测试的形式固化下来。循环往复这个过程。你会不知不觉的发现已经有几十个 case。这就是一张最基本的系统“安全网”。虽然这是系统集成测试,运行速度无法和 Function level 的单元测试相提并论,但是对于已存在的系统我们可以用这张网作为反馈系统。而新开发的功能就可以尝试 Function level 了。毕竟从 0 到 1 已经迈出一步了。
如何保证系统依赖方发生修改后系统依旧稳定?只要是一个产品,无论怎样都无法避免和第三方系统集成,小到一个 SDK API,大到另一个系统的 API Service。我们常规的做法一般是:看 API 文档;编写小程序学习 API。不过在看了《测试驱动开发》一书中提到的“学习测试(Learning Test)”这个模式之后,我眼前一亮。在编写小程序学习 API 的过程不也可以是编写 TDD 的过程吗?基于单元测试框架的断言,我们学习 API 可以明确的设定预期输入和期待输出。对于我们希望使用 API 的每一个使用场景都可以通过一个单元测试来实现。这样做除了学习 API 我们也留下来一套当前系统针对这个 API 使用场景的使用“安全网”。无论后续 API 版本升级还是有 API 调整,这一套安全网都能快速的反馈给我们是否对系统有影响。我还记得若干年前痛苦的对 SharePoint SDK 每个版本的改动做对比和评估,是多么的痛苦和无奈。居然被这么一个简单的办法解决了。
小贴士
上面这些做法是宏观的做法,落实到具体编程工具会有不同的实现方式。我使用 donet core 进行开发,下面介绍的一些小技巧会用他为例子:
就算是集成测试,为了运行速度和减少系统间调用复杂性,尽量使用内存模拟技术。WebApplication 可以使用 Asp.Net 自带的 TestServer。一个内存模拟 web 服务的轻量服务。无需真的 host 并监听一个 web 端口。TestSever 提供方法可以在系统关键位置注入服务,替换已有功能,方便测试场景的构建和使用。例如:认证服务的替换。数据库服务的替换(构建测试用例需要的特定数据库)。
对于一些复杂的第三方服务,可以通过测试替身(Test Doubles)进行替换,构建对应测试需要的服务返回内容。例如:Mountebank ;Postman Mock server 等。
回调方法如何测试。我这个系统存在一个请求上运行调用异步回调的场景。也就是当 Client 发送请求之后系统马上返回请求已接收的消息,但是实际工作在另一个线程,当结束后会调用 Client 的 API 将结果返回。这种场景如果简单验证当前系统接收到请求并不是重点, 主要的是回调部分内容才是真正的业务逻辑验证的关键。结合上面的结合上面 #1 的 TestServer,用依赖注入框架替换回调服务的内存实现版本,之后结合内存队列或者内存数据库来实现对回调方法传递数据的验证。
总结
测试驱动开发思想希望我们能为系统功能构建安全网。能够更有底气的应对系统重构。能够让开发的功能更准确的服务于需求。今天分享的对已有系统补充测试的过程主要是成本相对较高的集成测试级别,而并不是单元测试,但是能够达成目的的测试就是好测试。针对已有系统和新开发系统,我们要灵活应对。希望今天的分享对大家有帮助。
践行敏捷实践,让工作变得更美好。欢迎留言,交流落地经验。
参考引用
《测试驱动开发》
版权声明: 本文为 InfoQ 作者【Bruce Talk】的原创文章。
原文链接:【http://xie.infoq.cn/article/25ec2ef96b8fd5af1b973f3bf】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论