对已有系统如何开展TDD

TDD系列

Posted by Bruce Wong on April 30, 2022

前言

最近接手一个已经上线运行的产品,并负责后期的开发和维护。想着正好用这个过程尝试如何对已有产品进行TDD工程实践的可行性。今天就分享一下在这个过程中的感受和思考。
Kent Beck在他的经典《测试驱动开发》一书中提过,为已经能够工作的代码编写测试将是很难的一件事情,因为:

  1. 你的代码不是按照可测的标准进行编写。很难编写测试。
  2. 没有测试的反馈,后续改动、重构无法第一时间让你知道,例如哪些地方曾经好用而现在出现了问题。
  3. 如果你要先写测试,再修改功能代码,将会持续很久。因为你会遇到上面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进行开发,下面介绍的一些小技巧会用他为例子:

  1. 就算是集成测试,为了运行速度和减少系统间调用复杂性,尽量使用内存模拟技术。
    • WebApplication可以使用Asp.Net 自带的TestServer。一个内存模拟web服务的轻量服务。无需真的host并监听一个web端口。
    • TestSever提供方法可以在系统关键位置注入服务,替换已有功能,方便测试场景的构建和使用。例如:认证服务的替换。数据库服务的替换(构建测试用例需要的特定数据库)。
  2. 对于一些复杂的第三方服务,可以通过测试替身(Test Doubles)进行替换,构建对应测试需要的服务返回内容。例如:Mountebank ;Postman Mock server等。
  3. 回调方法如何测试。我这个系统存在一个请求上运行调用异步回调的场景。也就是当Client发送请求之后系统马上返回请求已接收的消息,但是实际工作在另一个线程,当结束后会调用Client的API将结果返回。这种场景如果简单验证当前系统接收到请求并不是重点, 主要的是回调部分内容才是真正的业务逻辑验证的关键。结合上面的结合上面#1的TestServer,用依赖注入框架替换回调服务的内存实现版本,之后结合内存队列或者内存数据库来实现对回调方法传递数据的验证。

总结

测试驱动开发思想希望我们能为系统功能构建安全网。能够更有底气的应对系统重构。能够让开发的功能更准确的服务于需求。今天分享的对已有系统补充测试的过程主要是成本相对较高的集成测试级别,而并不是单元测试,但是能够达成目的的测试就是好测试。针对已有系统和新开发系统,我们要灵活应对。希望今天的分享对大家有帮助。

践行敏捷实践,让工作变得更美好。欢迎关注我的公众号,交流落地经验。

参考引用