TDD基础导读

什么是 TDD?

测试驱动开发是一种软件开发实践,源于1999年Kent Beck《Extreme Programming Explained》一书中的测试先行这一概念。Kent Beck在2003年再次提到 – TDD鼓励简单的设计并激发信心。经过后期的发展,TDD已经成长为一门独立的软件开发技术,其名气甚至盖过了极限编程。

image

TDD采用了一种以终为始的思维方式,它依赖于非常短的开发周期的重复:先将需求转换为非常具体的测试用例,然后改进代码,从而让测试通过。实际编码过程体现为:在开发业务功能代码之前,先编写测试代码。测试代码确定了我们要验收什么以及如何验收,然后再去编写功能代码,当测试通过时,代表功能完成。

在实践过程中,人们常常将TDD区分为广义TDD和狭义TDD。广义的TDD是ATDD(Acceptance Test Driven Development),狭义的TDD,常常说的是UTDD(Unit Test Driven Development)。 本文所说的TDD更多指狭义上的TDD。

在实践TDD的过程中,我们对TDD形成了三层理解:

  • Task-Driven Development,任务驱动开发。
  • Test-Driven Development,测试驱动开发。
  • Test-Driven Design,测试驱动设计。

Task-Driven Development

任务拆解是TDD的一个关键的步骤。在开始编写进入代码环节之前,需要对业务需求做拆分,这个过程我们把它称之为Tasking。比如,一个用户登录的业务需求,我们大体可以拆分成如下几个Task:

  1. 假定用户名不存在时,当用户登录,则登录失败
  2. 假定用户名正确且密码错误时,当用户登录,则登录失败
  3. 假定用户名和密码都正确时,当用户登录,登录成功

通过上面的三个Tasking,可以总结出一个模式:Given When Then:

  1. Given 用户名不存在,When 用户登录, Then 登录失败
  2. Given 用户名正确且密码错误,When 用户登录, Then 登录失败
  3. Given 用户名和密码都正确,When 用户登录, Then 登录成功

Given When Then借鉴了BBD(Behavior Driven Development)里面的模式,它更加关注用户如何使用系统,即系统所提供的功能,从理解上更偏向于业务语言。对于一些技术人员,在初学阶段,需要多去练习,体会不同点,可以选业务人员视角索要一些反馈。

在TDD中,我将之称为Tasking三步曲:

  • Given,代表特定的业务场景
  • When,代表用户发生的行为
  • Then,代表行为产生的结果

Test-Driven Development

上一步Tasking拆分好的一系列Task,经过沟通澄清之后,就可以将这些Task翻译成测试。在翻译的过程中需要注意的一个核心点是 – 保持业务概念的统一(统一语言的应用)。如何理解这一点呢,来个看一个Task:

Given 一个有空位的停车场,When 停车,Then 停车成功,返回一张票据

翻译成测试代码后:

@Test
void should_return_ticket_when_parking_given_a_parking_lot_has_available_space(){
    ParkingLot parkinglot = new ParkingLot(1);
    String car = "布加迪"
    String ticket = parkinglot.park(car);
    assertNotNull(ticket)
}

在上述测试代码中,car和ticket都是用了字符串来表示(基本类型偏执),车和票据这两个业务概念没有体现出来,建议是使用CarTicket来代替这两个业务概念。

极限的TDD,会将以终为始的思维运用到极致。它提倡在写测试用例的时候,先写断言,然后一步一步往前倒逼驱动出来测试代码,比如上述的例子,经历的过程如下:

{
    // 1\. 我最终要验收的是停车票,代表停车成功
    assertNotNull(ticket);
}

{
    // 2\. 停车票是停车后返回的
    Ticker ticket = parkinglot.park(car);
    assertNotNull(ticket)
}

{
    // 3\. 停的车是我新开进来的车
    Car car = new Car();
    Ticker ticket = parkinglot.park(car);
    assertNotNull(ticket)
}

@Test
void should_return_ticket_when_parking_given_a_parking_lot_has_available_space() {
    // 4\. 停进了有空位的停车场
    ParkingLot parkinglot = new ParkingLot(1);
    Car car = new Car();
    Ticker ticket = parkinglot.park(car);
    assertNotNull(ticket)
}

这种方式在刚开始是很难做到的,但是通过多次可以练习,形成这种行为习惯,内化成思维习惯,也是益处多多,非常建议你去刻意练习。

Test-Driven Design

TDD并不会驱动出好的设计,TDD只会给你及时的反馈什么可能是糟糕的设计 – Kent Beck

在《百问TDD》中有一个问题能够很好地解释这一点,它是测试如何驱动设计?

为什么要 TDD?

及时澄清需求

Task Driven Development,我们拿到业务需求后会做Tasking,通过Tasking梳理业务需求,将一个大问题进行拆解后,从而能够深入理解业务细节。把一个个业务场景拆分罗列出来后,我们基本上对业务了如指掌,同时你也可以拿着拆分结果,去找业务人员沟通确认,以及澄清一些疑惑点。这个过程恰好体现了敏捷的核心精髓 – 及时反馈。

通过前期的反馈,能够避免因为业务需求理解偏差付出的无用功。

促进新人培养

如果团队来了几个新人,你需要带他们快速进入正常的业务开发,而迫于有限的精力,你不能做到同时跟几个人Pair。此时,教会他们Tasking是非常有帮助。你能够从新人的Tasking结果中能够快速确认他们对业务需求的理解是否正确,提前给出指导反馈,控制好方向。

另外,培养新人以终为始的这种思维习惯也是对他们的职业生涯有益的。

分离关注点

TDD的方式,写测试代码时,我们只关注功能验收,而且只关注特定的一个Task。写功能代码时,只用关注如何完成本次验收,即让当前的测试用例通过。测试通过后,你只用关注代码设计上,思考要不要将代码从刚刚可用的状态改良成简洁可用的状态。这个过程很好分离了我们的关注点,降低思维的负担,帮助专注聚焦,有效地提升开发效率。

构建保护网

TDD的一个必然结果产生是高价值、高覆盖的单元测试,为功能代码提供了一个保护网。这样一来,我们可以充满信心地进行代码重构,去改善我们的代码设计。

另外,相信你在项目上遇到过因为代码集成造成的功能破坏,这种情况多半是代码没有足够测试覆盖,集成时被破坏后没法及时发现。反馈延迟,必然增加维护成本,所以通过构建测试保护网,将大大缓解这种痛点。

减免调试

可能你会发现,采用TDD后,代码量会增加,貌似开发效率就会降低了。试想一下,如果没有单元测试,你就可能要手工测试(如果你还是一位负责人的专业程序员)。那么,手工测试时,在准备数据、启动应用、界面操作等事项上会花费你很多时间,而且大多数工作还需要重复去做,并且获得反馈周期拉长。

有了单元测试,你可以省去很多重复的手工操作,每次运行测试能够快速获得反馈,减免了繁琐的调试工作。

怎么做TDD?

TDD流程

我们常说,作为一个TDDer,要时刻切换三顶帽子(红帽子、绿帽子~ ~、蓝帽子),其实是说的是TDD的过程:红(编写测试)、绿(编写实现)、蓝(重构)

image

对于初学者,在做TDD的时候,有一个更加详细的流程供我们参考:

image

我才新写了一个测试用例,明知道它会运行失败(另外还有编译错误),还要运行吗?

我建议你运行一下。

首先,作为新手,这是一种节奏感和习惯的养成,一些行为习惯会潜移默化地影响你思考方式。其次,测试失败有很多种情况,你需要通过运行测试来获得反馈,这个失败是不是你期望的失败,比如:

  • 如期失败:是不是接口还不存在,编译错误?那我需要去实现我的接口
  • 如期失败:是不是新功能逻辑还没有实现,断言失败?那我接下就需要去实现新功能
  • 非如期失败:是不是不小心手误改了测试代码,导致其他的也失败了?那我需要当心了
  • 非如期成功,测试通过了,我的测试是不是写的有问题?那我需要当心了

三条童子军规

在美国童子军中,流传一条这样的军规:让营地比你来时更干净。Bob大叔在他的《代码整洁之道》中也引用这个词,提倡持续改进 – 作为一个专业高素养的程序员,要尽全力让项目代码随着时间流逝而越变越好。那么借用这个说法,在TDD中,我们也提倡三条童子军规:

  • 没有测试之前不要写任何功能代码
  • 一次只写一个刚好失败的测试,作为新加功能的描述
  • 不写任何多余的产品代码,让它刚好能让失败的测试通过就好

如果违反了这几条童子军规,会怎么样呢?

  • 违反第一条,如果先编写了产品代码,那这段代码实现了什么业务需求呢?你又怎么确保它真的实现了呢?手工测试,太费时间。后补测试,交付压力这么大,还是先写新功能吧。
  • 违反第二条,如果写了多个测试,测试长时间没法通过,会增加开发者的心里压力。实际上,伴随着知识的积累,业务细节的深入,Task会发生变更,测试会随之修改,那之前写的测试就成了累赘。
  • 违反第三条,产品代码实现了超出当前测试的功能,这部分代码就没有测试的保护,又回到第一条了。另外,多些的代码可能是不存在的需求,增加了没必要的复杂性。

据我观察,别说TDD的初学者,就是有一定经验的TDDer,也很难时刻做到一次只编写刚好让测试通过的功能代码。我还见过有些人一上来写上10来个测试用例。那么在实践练习的过程中,在开始守(守、破、离)的阶段,可以时刻用这三条军规提醒自己。

TDD相关书籍

参考阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容