嵌入式自动化单元测试(1)-TDD

TDD definition

Wikipedia 给 TDD 这么定义:

Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved to pass the new tests, only. This is opposed to software development that allows software to be added that is not proven to meet requirements.

[Notice:requirements]

TDD 最早是 Kent Beck 在 Extreme Programming(极限编程)中提到的。他给了如下步骤:

  • write a test for the next bit of functionality you want to add

  • write the functional code until test passes

  • refactor both new and old code to make it well structured

TDD 的定义和步骤看起来很简单:需求被转化成一系列明确的测试例,然后撰写代码使测试依此通过,最后重构使得增量代码和存量代码和平共处,满足设计规范和原则。然后不断迭代,直至整个项目完成。

TDD confuse

TDD很容易误解,我们先看常见的几个误区。

  • Test belong to QA,Development belong to RD

很多公司研发和测试团队泾渭分明, TDD时各管各,从一开始就走向错误的道路。TDD 是开发过程不可分割的一环,这里的 T 的目的是做需求分析和设计,和 QA 没有太大关系。

  • The more test cases ,the better

在 TDD 过程中,最忌讳的就是写下过多的 case,尤其是和需求毫不相关的 case。写 case是有成本的,在 TDD 过程中引入太多的 case意味着三种可能:

  1. 没有彻底理解清楚需求;

  2. 过度设计case;

  3. 不知何谓TDD。

  • TDD 不需要软件设计

这种想法跟敏捷开发不需要设计一样可笑。没有任何一个做 Agile 的,或者做 TDD 的会说不需要设计。大家都是嘴上说设计很重要,执行起来却变了味:花时间做设计还叫什么小步快跑,不断迭代?越是走 TDD 的路子,前期的需求分析和设计就越重要。

How to TDD

开启 TDD 之旅的第一步,是明确需求。

我们以一个通用的 news feed 为例来阐述 TDD。在 social network 中,news feed 是其他用户的行为对你产生的动态的一个集合。如果 Bob follow 了 Alice,Alice 的一颦一笑都会被 Bob 的时间线捕捉到。这就是基本的需求。

那么怎么解构这个过程呢?比如说 Alice 喜欢了一张小猫咪的照片。我们可以这样描述:

Alice liked photo

有时候我们进一步需要知道这个照片的来源,可以这么描述:

Alice liked photo on Cynthia's album.

我们把这个描述泛化:

Alice (actor) liked (verb) photo ... (object) on Cynthia's album (target).

从中抽取出四个要素:actor,verb,object,target(optional),通过这四个要素,我们可以描述一个用户的行为。

actor 一般可以是用户,但有时也可以是系统(或者机器人)。我们可以用一个全局的 id 来区别 actor,甚至为 actor 加上类型。

verb 是描述用户究竟可以有哪些动作,这些动作产生什么样的影响?常见的 verb 有 like / unlike,follow / unfollow,create,delete,add,remove 等等。follow / unfollow 是一种特殊的 verb,通过这样的动作我们可以构建用户的 subion 表。

object 描述被 verb 处理的对象。它们可以是任何东西。比如说:

Alice created comment "bla bla bla"

这里 object 就是一条评论。object 需要有 id 和类型来唯一标志可以从何处访问到这个 object。

target 可有可无。它往往是 object 的集合。比如 Alice added a song "abc" to album "best songs"。target 就是这个 album。当这个行为出现在 Bob 的时间线上,Bob 可能对这个 target 也会感兴趣。

当用户的行为可以用这样一个数据结构描述后,我们接下来需要考虑当收到这样的数据结构后,系统怎么处理。自然,第一步我们想到的是:actor 的 followers 会接受到 actor 的行为。紧接着,我们会问,followers 怎么产生?oh,如果 verb 是 follow,则把 actor 添加 object 的 followers 中,反之移除。我们把这个流程图表化:

这样这个系统清晰多了,我们可以看到系统分成几个部分:

  1. activity receive and persistence - 当一个行为产生后,外部系统会调用 news feed 来创建 activity;

  2. feed generator - 当 activity 创建成功后,它会扩散到所有 subscribers 那里生成 feed;

  3. subion generator - 当 activity 是某种特定 verb 的 activity, 我们维护 object 的 subion 表(添加/删除)。

以上过程可能是我们查阅资料得出来的需求设计,也可能是大家一起讨论出来的。到现在为止我们还没有做任何和 TDD 相关的事情,但这个过程对于做 TDD 是绝对不能少了。

有了大的需求分析和设计后,我们可以开始细化每个部分的设计。TDD 在这个阶段才应该现身。现身过早,很容易导致返工,现身过晚。。。都开始写代码了,那还是 TDD 么?

Interface definition

根据我们之前的设计,现在我们开始做第一个任务:activity receive and persistence。很多人拿到一个任务后就着急开工,但做 TDD,第一步是定义这个任务涉及的子系统和外界的交互,或者说接口。根据我们之前的设计,我们可以设计这样一个接口了:

没错,我写了一个测试例。这个测试例告诉我我希望以何种方式使用新创建的接口。虽然我们是在写测试例,但我们在思考几件事情:

  1. 创建一个 activity 的接口是什么?

  2. 它允许什么样的输入(接收什么样的参数)?

  3. 它返回给调用者什么样的结果?

是的,写下这个测试例的过程就是接口设计的过程。这是我认为 TDD 帮助最大的地方 —— 在写代码之前先考虑清楚接口。

Interface review(Optional)

这个过程视接口的重要程度和工程师的水平可以省却。大多数 code review 流于形式,去做那些本该由 static analysis 完成的事情。如果资源和时间极度紧张,宁可不做 code review,也要做好 interface review。因为 interface 糟了,局部再美的代码在一坨屎的全局下也是插在粪堆里的鲜花,凋零是迟早的事;如果 interface 对了,局部再遭的代码也只是一片花海中的 shit 而已,捡出来扔了即可,无伤大雅。

如果我们打定主意要做 interface review,那么 TDD 简直是你的不二之选。有些 interface review,往往是在 word 文档,或者是 wiki 中 review 的,这种 review 的最大问题是即便 review 通过,也可能出现文档和代码不一致。日后接口改变,谁还记得要改之前那个已经不知道在哪的文档了呢?

为了保证 interface 是 review 的结果,最好的办法就是把 interface 使用的方法通过Test case的方式来表述,这不正是 TDD 干的事情么?

用 TDD 做 interface review ,只需定义好测试例后,先别着急写代码,给相关的人发个 PR,看看别人有何评价,然后再进行下一步。

Development

在 TDD 中,开发的环节是若干个小的 TDD 组合起来的。其实任何软件开发的过程都是一个需求设计开发测试自我迭代的过程,和分形几何有异曲同工之妙,都是无极中圆环套圆环的思想。大的设计/接口之下包含若干小的设计/接口,然后在小设计/接口下再包含更小的设计/接口。TDD 更强调这一过程。因此,在开发的各个阶段中,可能需要不断地为你的更加细分的接口设计添加新的测试例。一般而言,TDD 应该涵盖这些层次的接口的测试:

  1. 「User」级。对于很多项目来说,用户级的接口是 API。这里可能是 Rest API,GraphQL,RPC,私有协议等等。

  2. 「App」级。这里说的 app 并非指一个单独的应用程序,而是逻辑上的概念。一个系统可以逻辑上分解成若干个内部的 app,它们互相作用,最后构成了这个系统。app 间如何互相调用,非常重要。

  3. 「Module」级。我们只需要关心模块的公共接口。私有接口无所谓。

这个层级应该根据项目和系统的需要随意调整即可,否则可能会导致写出过多的 test case ,以至于成为累赘。

我的经验是,对系统中不确定的,或者变化大的部分,不要引入过多的 case,而对于系统中确定的,或者接口已经发展稳定地部分,不妨把 TDD 延伸到模块级。

这里引申出一个问题,我们在开发中究竟采取自顶向下,还是自底向上的方式?

我对此更倾向于自顶向下。因为它往往和我思考的过程一致:我喜欢在一个大的设计完成,大方向确定后就开始自上而下开发,细节在开发的过程中逐步确定;当然有人也喜欢全部确定下来了再开始构建一个个 building block,再将他们组装起来。

回到我们之前的 activity create 的接口的实现:

我把这个过程分成了的三个小步骤:生成数据,持久化,通知。这样,这个接口的实现就有了。我们可以进一步思考这几个小步骤的接口:

  • generate 接收用户的参数,验证后生成一个 activity 的数据结构
  • 然后为这几个小接口写 test case(可选),并继续细化每个步骤。

对于一个系统,我们可以将它看做是输入到输出的一系列 transformation。这样,很容易把问题分解成一个个小的问题,写出清晰可读的代码。

Some Questions

- TDD 是否适用所有场景?

不是。前端的项目做 TDD 有些吃力 —— 这也许是因为我不是一个合格的前端。我觉得用一个(或者若干个)test case 来表达前端的页面似乎不可行,尤其是 Single Page Application。所以我们需要把表现层和逻辑层分开,对 MVC 中的 M 和 C 做 TDD,而放任 V。绝大多数项目都可以用 TDD 的思维来分而治之。

- TDD 中的 T 是 unit test 么?

按照 Kent Beck 的定义,是。但是我觉得这不准确。显然,对于用户层的接口的 test case 很多时候已经超越了 unit test 的界限,更像 integration test / functional test。但是我觉得纠结是否是 unit test 并不重要,重要的是你的 T 是否在反映你的设计,你的接口。纠结于 UT 与否,只会自陷泥潭。

Mock code

我们在做开发时,经常会遇到所做系统的调用方在接口确定后需要尽早开工,以便于项目尽快完成。比如后端提供 API,前端实现 UI,这时我们需要为前端提供 mock API。在正常的开发流程中,mock API 会被正常的 API 逐渐取代,直至完成历史使命,被扫进 git history 的故纸堆里,成为沉没成本。在 TDD 中,我们可以让 mock code和 test code采用相同的 fixture:

这样,即便以后 create 被真正的代码取代,这个测试依旧是一笔有益的投资,不是沉没成本。

Test code organization

TDD 的不同的阶段写下的 test case 的级别是不一样的。通常我们应该把顶层的,用户级别的接口放在一个目录下,app 级别的按 app 名放在不同的子目录,模块级的按模块名放在不同的子目录,不要混在一起。用户级别的接口应该是最稳定的,添加新接口无妨,但是如果已有的接口要改变,我们需要从中分析原因并吸取经验。然后想想下次怎么能做得更好。app 级和模块级的接口变化可以不必那么慎重。如果你发现你的模块级接口的 test case 总变,那么你可能需要考虑在初期省却这部分的 test case —— 它标志着你要么设计出了问题,要么有些过度 test 了。先解决别的问题,再考虑 test 的事情。

Test code doc

Test code文档化很有必要。比如在介绍代码如何使用时,可以通过将文档链接到相关的Test code上,让调用者对代码的使用有个更清晰地认知。

这里推荐一种更好的方式(doctest) ,在代码的文档部分,嵌入如何调用该代码的示例代码,这部分代码进而变成测试的一部分。doctest 早先见于 python,现在几乎所有语言都有工具支持。TDD 的用户层接口可以考虑使用 doctest 完成 test case 的撰写。

转载于:https://juejin.im/post/5cdd0846518825035b3d0948

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值