B站视频版
如果给我一个小时来砍树,我会先花 20 分钟来磨刀。
---- 林肯

在上一篇内容 TDD 实战(1) 通过一个案例来展现了 TDD coding 的过程,编码之前首先要做的事情就是确保已经准确理解了需求。TDD 也不例外如果需求不能获取准确后续用什么实践都无法完全交付想要的。
工作中有 BA 人员来挖掘需求,DEV 将业务需求转化为软件功能,其中 Tasking to Action 是开发之前的利器,也是 TDD 过程的第一步。
Tasking to Action 能够帮助开发人员确定自己理解了业务需求,并将需求分解为若干个可以小步实现的任务,工作中在做卡之前,首先就是对要做的卡进行 Kick-Off,Kick-Off 的过程中 DEV、BA、QA共同进行的,DEV 可以针对分解之后的Tasks 与 BA、QA 达成用户故事卡理解上的共识,并调整 Task,从而确保需求的实现是建立在共识下的。
因此,Tasking to Action 能够很好的将质量验证前置,并为质量内建提供帮助。
01,如何进行Tasking to Action?
当解决一个复杂的问题的时候,你会怎么做?是不是先将复杂的问题进行拆解,生成一个个简单的问题,当一个个简单问题解决,那个复杂问题也就解决了。
Tasking to Action 就是对问题进行拆解的过程,将业务需求分解为有上下文、有行为、有结果的若干个 Task。每个Task 都是简单的、能够快速实现的、代表具体场景的。
在做 Tasking to Action 的过程中发现遗漏的、不确定的需求,User Story 的上下文以及涉及到的边界,能够帮助我们更加准确的进行Tasking 过程,避免遗漏。
Tasking 之后的生成的 Tasks 我们可以通过 BDD 的方式来进行描述。
02,Tasking to Action 使用 BDD
工作中我们要描述清楚一件事情,通常会介绍四部分内容:
什么背景下?
遇到一个什么问题?
我的解决思路是什么?
这件事的主要干系人是谁?
通过这样的格式我们能够轻松的完整的让其他人了解清楚的来龙去脉。
BDD 提供了一种良好的描述信息的格式,能够清晰的描述一个用户故事的某个场景的上下文和结果。就是我们常说的 Given、When、Then。它们代表了不同的信息Context、Event、Outcomes,如下图所示是一个摩托车打火的过程。
为了进一步说明,我们举个例子,需求如下
"我需要将大象装进冰箱。”
拆分后结果如下:
01. Given:一只大象
And 一个比大象体积大的冰箱
And 冰箱门打开;
When:将大象装进冰箱;
Then:大象被装进冰箱;
02. Given:一只大象
And 一个比大象体积大的冰箱
And 冰箱门关闭;
When:将大象装进冰箱;
Then:装载失败;
03. Given:一只大象
And 一个比大象体积小的冰箱
And 冰箱门打开;
When:将大象装进冰箱;
Then:装载失败;
04. Given:一只大象
And 一个比大象体积小的冰箱
And 冰箱门关闭;
When:将大象装进冰箱;
Then:装载失败;
使用 BDD 我们能够清晰的描述清楚任何一个 Task 的背景、每个Task 的动作、每个 Task 的期望的结果。
如果 Given 中的条件比较多,可以使用
And
关键词来连接
03,Unit Test with BDD
BDD 在编写代码时同样在三方面带给我们帮助:BDD 风格的方法名、BDD 风格的代码块、BDD 风格的断言。
(1)BDD 风格的方法名
@Test
void should_got_a_qrcode_when_store_bag_given_a_bag_and_a_store_content_ark_with_10_space() {
...
}
BDD 风格的方法名同样包含 given、when、then 三部分,常用的格式是:should…when…given。
采用 BDD 风格的方法名虽然长度长,但是语意非常清晰明了,一眼看上去就知道这个测试的上下文、行为、期望的结果。
(2)BDD 风格的三段式代码
单元测试的代码块同样可以分为 given、when、then 三部分,如下
@Test
void should_got_a_qrcode_when_store_bag_given_a_bag_and_a_store_content_ark_with_10_space() {
// given
Bag bag = new Bag();
StoreContentArk storeContentArk = new StoreContentArk(10);
// when
QRCode qrCode = storeContentArk.store(bag);
// then
then(qrCode).isNotNull();
}
上面便是我们常用的三段式风格。
Given:指的是该测试的上下文信息
When:指的是要值测试的方法的行为
Then:断言,验证执行结果。
工作中可以直接去掉其中的注释部分,约定俗成 given、when、then 通过空行来分割。
@Test
void should_got_a_qrcode_when_store_bag_given_a_bag_and_a_store_content_ark_with_10_space() {
Bag bag = new Bag();
StoreContentArk storeContentArk = new StoreContentArk(10);
QRCode qrCode = storeContentArk.store(bag);
then(qrCode).isNotNull();
}
(3)BDD 风格的断言
上面的例子或许你已经注意到,断言部分使用了
then()
这是 一种 BDD 的代码风格,AssertJ 中提供了这种断言的风格,可以参考
org.assertj.core.api.BDDAssertions
类中的
the()
这样的风格的断言十分清晰的结果应该如如何的。
04,实战 Tasking to Action
在 TDD 实战(1) 中我们围绕储物柜的需求来体验了 TDD 编码部分的过程,但是并没涉及到详细的 Tasking to Action 的过程,本文我们将基于储物柜的需求进行 Task 分解。
需求:
现在一家公司提供储物柜(Store Content Ark)功能。
用户通过将包(Bag)存入到储物柜后得到一个二维码(QR Code),
通过二维码能够从储物柜取出包。
将该需求进行分解之后,形成下面5个 Tasks:
01 Given: 一个包
And 一个拥有10个空间的储物柜
When:存包
Then:存包成功并拿到二维码
02 Given: 一个包
And 一个满了的储物柜
When:存包
Then:存包失败
03 Given: 一个存过包的储物柜
And 一个二维码
When: 用二维码取包
Then:取到自己存过的包
04 Given: 一个存过包的储物柜
And 一个使用过的二维码
When: 用二维码取包
Then: 取包失败
05 Given: 一个存过包的储物柜
And 一个非法二维码
When: 用二维码取包
Then: 取包失败
通过 Tasking 我们产出了 5 个 Tasks,使用这些 Tasks 我们可以在故事卡 Kick-Off 的时候与 BA、QA 共同确认已经理解了需求。
确认需求后的下一步就是将 Task 转化为具体的测试代码,同通过测试小步并驱动出业务实现。详细的可以参考 TDD 实战(1) 的驱动过程,也可以关注后续的文章,如何一步步驱动出代码实现。
05,常见问题
(1)TDD 之前必须先做 Tasking to Action 吗?
是的。
只是形式可以针对自己选择更高效的,我平时使用上面的形式,因为结构和条件都很清晰。有时也会在本子上写写画画,还会借助二维表等进行逻辑梳理,偶尔也会使用脑图,甚至简单的问题在脑中就已经完成了 Tasking to Action… 总之选择选择自己认为最高效的。
使用 BDD 的风格产出 Tasks 是一种将思路显性化的过程,虽然确实有少部分人的思维非常缜密,但是掌握这种可视化的方法能够让大家处理问题能够如虎添翼。
至于是否进行可视化展现,取决于可视化的价值,我们会选择 ROI 高的来做。
(2)Tasking To Action 的结果必须是用 BDD 的方式吗?
不是。
如上一个问题中提到的,可以用脑图,二维图表等,甚至一段清晰明了的文字。
为此我用 Alfred 来定义了一些模版,省略了部分重复的工作,来让效率更高。
(3)BDD 风格的方法名很长,必须这样吗?
不一定。
首先方法名长但是清晰明了是没有任何问题的。试想当读一个抽象的方法名获取到的有效信息多,还是读一个很具象的有效信息多,在单元测试中我相信是后者对这块测试代码描述的更具象,因此长方法名名不是问题,而不能清晰的描述测试意图则成了问题。至于读代码的成本可以亲身体会。
但是长方法名可以用一些手段来变短,并消除一部分重复。比如可以用 @Nested 来归组,这样重复的上下文,就不需要反复提及,因此方法名也就短了。但是同样需要能够清晰描述。
不建议使用注释的方式来描述意图。首先注释和方法名都是体现测试方法意图的,明知重复何不一次完成。随着代码的增多,原本的测试有些情况下会变动,注释往往忘记修改,何不省掉注释用方法名来体现呢。
(4)常见到 should … when … 少了 given,这样写有问题吗?
不推荐这样写。
如果你早就熟练操作方法名,别切由相关理论、实践支撑,那么没有问题。但是如果没有,只是照搬别人的写法,那么不推荐这种写法,我试着问过一些用should…when…这样写法的同学,首先他们意识到given和when 混合在一起了,其次并不能第一时间说出 when 中的动作是什么。这就是涉及到读代码的成本了,读代码花了更多的时间,这些短小的时间偏短后续会积累成很长的时间。
所以推荐使用 should…when…given…这样的结构。
写在最后,如果对 Tasking to Action 非常熟悉,我们可以改进其中的步骤,从而提升效率。但是如果对其并不熟悉,例如看到文中的例子,看上去简单,但是上手的时候却不得不回来看看例子的,还是老老实实先练习几次,知道是什么,为什么这么做了,再根据自己的经验来改进。