cucumber你需要了解的一些概念
如何描述场景
如何写出让利益相关人员阅读,同时又能被测试的规格说明。
Fred Books在《没有银弹》(No Silver Bullet)中提到:
构建软件系统,最困难的部分就是准确地决定到底构建什么。
所以开发和利益相关人更好的沟通是避免此类事件浪费的关键。需要一种有助于促进沟通的技术,所谓cucumber,就是来阐明我们想让开发做什么,想让软件成为什么。
在于相关人员沟通的过程中,他们能提供于我们有用的反馈和想法,他们能提供于我们所谓的验收标准和条件,我们创造系统告诉相关人什么才是真正能被验收的。如下需求描述:
应用只能允许用户输入合法的信息
这样的需求的描述本身没有问题,问题在于它提供的信息给产生歧义和误解留下了太多的空间,没有精确度。什么样的输入信息是合法的?
用户输入的手机号必须是11位,并且以1开头的数字,提交时应当显示"请输入正确的手机号",以提示用户正确的输入方式。
而这样的需求描述很明显是更加具体的,开发人员可以瞬间get到所有的条件和逻辑,相关人员也能更清楚得表达自己的需求。
如果能将这样的需求描述表达得如此清晰,以得到所有的逻辑条件,那么假如这些实例说明又能让计算机精准地阅读,并帮助我们完成应用的验证,又会如何呢?
我们在cucumber中用到了Gherkin,它提供了一种轻量的结构能让开发人员和业务相关人同时理解的自然语言,同时也是一种编程语言。
Scenario: Give me a cup of coffee
Given I pay for the coffee
And they made a cup of coffee
Then they gave the coffee to me
有人认为Given
, When
, Then
, And
and so on,用起来比较繁琐,其实我们还可以使用*
来代替,比如上述的场景可以描述为:
Scenario: Give me a cup of coffee
* I pay for the coffee
* they made a cup of coffee
* they gave the coffee to me
这样的功能描述是非常精益的文档,他的描述也没有限制,如果你觉得很难描述应用的某个特性,那么如下的模板是个非常 nice 的开头
- 系统处于某种特定的状态
- 触动一下系统
- 检查系统的新状态
这样的一个场景,通常都是一个小小的故事,能描述系统的某个特性,或者这个应用能够做到的事情。你可以尝试使用Given, When, Then描述你手头在做的日常事务,比如做饭,开车,撕逼,打架、、Gherkin能描述一切场景,并且你也能熟悉如何使用它描述你的系统。
我们描述一个特性通常会用到5-20个场景来阐述,但是有一个非常重要的概念你需要掌握:
每个场景都必须有独立的意义,且能够不依赖任何其他场景独立运行。
每个场景构成联系并不会使cucumber报错,并且cucumber也没有理由阻止你这么做,但这确实是一种很差劲的实践,如果这么做,最终得到的场景描述可能会难以理解甚至出现难以预料的失败。
这是一个不成文的建议,初期可能会对我们描述场景产生阻碍,我们时常太依赖自然语言的思维,但是应该学会描述这类系统测试场景,在不同的场景之间构造一些脆弱的依赖不是一个很好的尝试。
如何编写步骤定义
cucumber扫描每个步骤的文本,是可以通过正则表达式匹配的,假如我们能抓住系统特性描述的规律,可以帮助我们可以用很少的模式处理大量的场景。
比如说:
- 点击xxx
- 存在xxx
- xxx的文案是xxx
- 存在图片xxx
- 跳转到xxx页面
- 页面向上滑动xxx
...
编辑一套常规操作步骤基本可以描述80%的系统特性场景。
定义步骤的目的是为了取得一定的结果,为了一种测试工具,cucumber如何告诉我们它是通过还是失败呢?
同大多数测试工具一样,cucumber使用异常来表达测试失败。一个完整的cucumber运行过程如下:
在cucumber中,运行记过最终处于以下状态之一:
- undefined
- pending
- failed
- passed
未定义 undefined
表示cucumber无法匹配当前步骤的定义,即被标记为未定义。对于未定义的步骤,cucumber会在终端显示黄色的步骤定义说明。
待定 pending
对于只实现了一般的步骤,整个场景会停止,剩下的步骤会被跳过。cucumber依赖开发人员告知步骤定义是否完成,如果你的步骤定义还在开发中,可以将该步骤设置为pending,告诉cucumber步骤失败了,但是这是一种特殊意义的失败,步骤定义还在实现过程。
为什么会有pending这种状态呢?其实这也是一类常见的场景描述,我们在玩手游的时候,偶尔会看见部分功能尚未开放的指示,一为诱导用户等待更新,二为应用特性仍在开发中的提示。同理,pending状态的步骤定义成为了我们的待办事项 todo list。
失败的步骤
失败的步骤往往是因为抛出了异常,终端会显示红色标记并停止该场景。
失败的原因有两种:
- 系统bug或步骤定义的error
- 步骤定义中使用断言来校验系统状态,断言未通过
嵌套步骤
保证一个场景的每个步骤在同一个level,不要过度细化,避免过多的噪音。怎么理解这句话呢?我们来看一个例子:
Scenario: Driver the car
Given I have a key of the car
When I take the key
And I use the key to start the car
Then the car started
Then I drive it out
在这个场景中,我们关注的细节在于,前置状态,触发状态和后续状态,这也是一个典型的cucumber场景描述。然而这里有太多关于触发状态(start the car)的噪音,以至于我们无法注意到真正重要的部分,过于细节化的,或命令式的场景存在风险。于是我们需要一类能抽象语义的步骤。
嵌套步骤重构
我们可以用一句简单的语义概括以上的触发状态(start the car)。
When I start the car
所以最终的场景描述应该如此:
Scenario: Driver the car
Given I have a key of the car
When I start the car
Then I drive it out
是不是结构更加清晰,场景的主要部分更加突出了?场景的行为没有发生任何改变,只是特性的描述上升到了更高的抽象层次上了。
嵌套的危险
嵌套步骤是简易上手的,甚至在设计场景的过程中,会自然而然地想要抽象化一系列操作,对于开发人员来说,这等同于抽象一个通用的函数方法。有一位经验丰富的cucumber用户Andrew Premdas,分享了他的故事:
嵌套步骤不利于你成为一名更好的程序员,实现步骤定义所涉及的编程工作总的来说非常简单,每一次使用嵌套步骤,你就失去了一次和底层代码直接交互的机会,同时也失去了理解这些与系统对话的代码的机会。你变得越来越依赖于嵌套步骤,而不是伴随着新的场景持续学习并成长。随着你越来越多地使用嵌套步骤,这种依赖也会增加,而步骤定义的复杂度也会一起增长。
需要记住的重要一点是:你可以在每一个步骤定义的代码中使用提取方法这一重构技巧。这已选择永远比嵌套步骤更可取,因为发现、阅读及修改代码比处理步骤定义更容易。当你可以把一些方法抽取出来时,何必费劲折腾嵌套步骤呢?
实际场景汇总,我们嵌套得越多,实现场景就越难,而摆脱嵌套步骤可以让事情更加透明。
如何保证feature文件夹的条理性
很久以前cucumber作为工具诞生的时候,它的名字是RSpec Story Runner,那时候语言描述的测试使用story
作为扩展名,为什么后来改成了feature
呢?
用户故事是用来做计划的工具,每个故事包含可以划分优先级、构建、测试及发布的一点功能。故事发布后,我们不希望它在代码中留下痕迹,我们通常会重构以清理涉及,这样代码就可以重新接受用户的新行为,新故事。
我们也希望cucumber如此,特性描述应该描述系统今天的行为,它们不需要记录系统构建的历史,那是版本控制系统的事情!否则我们的feature会像下面这样:
features/
story_0731_a.feature
story_0807_b.feature
story_0814_c.feature
也许只是针对同一类功能特性,我们生成了N个碎片化的特性,这无法成为系统文档,一个故事可能匹配了一个特性,另一个故事可能是将这个特性修改和优化。比如登录方式的改变,下单方式的改变。合理的方式应该是更新原有的特性,描述新的用户故事。