时间回到2022年,我参与了一个使用了Flutter技术构建的Web前端项目。在这个项目上,我们小组的目标是实施Flutter前端自动化测试。
彼时,Flutter 2.x刚在Web端发力不久,Flutter Web上的应用和生态才刚刚开始,而在这一切激进的技术栈上构建一套自动化方案的需求又迫在眉睫。
在技术选型上,我们使用了类Cucumber测试的方案,使用Gherkin语言构建一套自动化语言步骤库。Gherkin语言有时候又被称为小黄瓜语言,它是第一种有着类似自然语言可读性的业务语言,用来描述业务行为,而不必关心具体的实现细节。它也是一种领域特定的语言,用来定义Cucumber格式的测试。
通过不断地改进,我们使得这些步骤既具有自然语言通俗易懂的可读的特性,又具有自动化步骤的可执行性,用这套步骤,我们最终用它统一了手工测试用例和自动化测试用例的书写,执行,管理。
Gherkin语言其实可以使用不同国家语言的单词和语法书写,但和其他编程语言一样,我们这里还是使用英文单词和文法。
当定义步骤库的时候,使用Gherkin语言去描述业务或者用例可以有不同的风格,典型的有“文档式Gherkin”和“动作式Gherkin”这两大类。
文档式Gherkin往往用来描述“应该做些什么”。所以经常用来描述软件需求,产品期望行为。
比如一个步骤是:“当创建了一个新用户的时候,那么他会出现在新用户列表中”。这种风格的好处就是可以快速书写出一个结构合适,方便理解的软件文档。当这种文档式Gherkin语言写的测试执行失败的时候,往往代表了产品的实际行为和文档上的期望行为发生了背离。当然文档式Gherkin语句也有其缺点,比如自动化实现起来,某些语句需要验证的范围会非常大,执行复杂,且随着测试的增加,步骤库里的步骤数量也会快速地增长。
相比文档式Gherkin,动作式Gherkin描述的是“如何做些什么”,由于动作式Gherkin关注的是每一步具体做什么,所以常用作写测试用例。如一个步骤是“当点击含有Submit文字的按钮,那么Successfully文字应该可见”。
动作式Gherkin语句的好处就是目的单一,每一步需要验证的点很小,当然与之而来的缺点就是要完成一个用例的书写需要很多步骤组成,用例里的步骤会很多,用例变得很长。且每一个动作对测试场景的覆盖率都不高,需要完成测试覆盖率要很多步骤一同拼凑起来。
由于我们需要大量自动化测试用例,所以我们更倾向于使用动作式Gherkin,虽然最终我们使用了动作式的Gherkin语言定义了自动化步骤库,但我们还是先了解一下文档式Gherkin的风格。
文档式Gherkin使用描述性的措辞,聚合了具体的动作。这使读者能够快速理解一个场景,并掌握文档中描述的软件功能。使用文档式Gherkin语言写的软件需求或者测试,并由自动化实现执行后,这个文档基本上不会过时。因为一旦软件实际行为和需求文档的描述发生了背离,那么自动化执行需求文档上的Gherkin语句的时候,测试就会失败。而这些测试往往都是以天为粒度去执行的,如果今晚你提交了代码改变软件行为而没有更新需求文档,当晚的自动化测试流水线就会失败红掉。如果你的项目有流水线”红不过夜”(导致流水线失败变红的问题不留到第二天而是当天内解决)的规定,那么恭喜,今晚就必须把文档上由Gherkin语言书写的测试修正。
使用场景:书写可测试的软件需求说明书
例子:
When the admin creates a new user
Then the user list should contain the newly created user
Scenario是Gherkin语言中的关键字,通常代表一个场景。作为一个经验法则,一个文档式Gherkin写的场景通常由3-5个步骤组成。有时候,就连包含Given的步骤也不需要,那么便只需要2个左右步骤了。当然,长的场景可能包含了多于5个Gherkin步骤,此时便可能说明了这个场景需要拆分了,让更多更小的场景去覆盖每一个需要关注的点。
同时,前置条件也是可以隐式说明的。比如,在描述产品行为的时候,对于每个与登录页面本身无关的场景,可以预期用户已经登录了。每一个场景一般只应该包含一个“Then”。
如果有多个包含Then的步骤,那么就说明这个场景有多个AC(Acceptance Criteria)。如果Then语句执行失败,那么此AC便校验失败了,测试便会停止,那么后面的AC步骤也不会去执行验证,于是在失败的路径下多个AC便失去了意义。当然这个规则也不是个教条,比如当两个AC相互依赖,他们最好同时验证。比如分开验证两个AC都时候时间成本,外部资源成本非常昂贵,那么也是可以放在一起写多个Then语句的。
当描述一些行为的时候,应当使用主动的语态,比如 “the user does X” 而不是 "X is done by the user"这种被动的语态。
在Gherkin中定义包含Given语句的时候,要使用过去时时态,因为这表示测试之前发生的一个前置条件。如:“Given the user was logged-in” 。
在Gherkin中定义包含When语句的时候,要使用现在时时态,这代表测试执行的时候发生的,如:“When the user cancels the form”
在Gherkin中定义包含Then语句的时候,用情态动词写成期望例如:Then the form should be closed。这强调了我们不是预测SUT将如何表现,而是描述我们对它的期望。
使用这些角色名称:如 “Users”、“Admin”、“Guest”,而不是 “I”。这可以增加一个场景的重点,让它专注于某一个角色,以便更容易阅读理解。在后续步骤中,要么重复角色名称,要么使用代词They来代表这类角色。
例如:
When the Admin starts the creation of a user
Then the Admin should be asked to confirm the creation
又如:
When the Admin starts the creation of a user
Then they should be asked to confirm the creation
否定词会大大改变句子的意思,但很容易被忽视。把否定词写成大写字母,便可以强调它们。
例如:
“The the text “Welcome” should NOT be visible” 或 “The user should NOT exists”
作为技术人员,往往具有很强的工程师思维惯性,产出物也是有鲜明的技术标签。所以从业务的视角来看,并不是那么对用户友好。我们定义的第一版步骤库便是如此。比如Flutter项目中所有的对象都可以加上类似于id的key属性,用来查找这个唯一的对象元素,如果在步骤中要用这个属性来寻找对象,那么步骤变成了类似:
When the element with key “userNameTextField” is filled with text “[email protected]”
这么定义出来的步骤可能有如下问题:
作为业务人员,更希望在步骤中隐藏所有技术细节,方便使用。所以我们做了如下改进:
于是,上面的步骤就变成了:
When the text field “user name” is filled with “[email protected]”
我们使用动作式Gherkin定义了用例,通过总结,有如下经验。动作式Gherkin语句用每一个参数化的步骤描述一个行为,这种风格使得步骤库的体积不必非常大。因此也减少了自动化框架步骤的开发和维护工作量,每一个步骤尽量和公共组件进行互动,也保证了每个步骤的重用性非常之高,所以,一旦需要新测试,用现有的动作式步骤库书写后这个case就可以立即运行了,也不需要实现一个新的步骤。
使用场景:写测试用例
例子:
When the button ‘Create User’ is clicked
And the text field ‘Last name’ is filled with ‘Jim’
And the text field ‘First name’ is filled with ‘Green’
And the button ‘Save’ is clicked
Then the ‘user list’ should contain the text ‘Jim, Green’
由于动作式Gherkin不可避免要使用更多的步骤,所以动作式Gherkin的测试长度一般都会更长一些,但是这并不代表一个测试可以写很长很大。同样地,一个测试还是需要遵循单一原则,最好覆盖一个测试点,在覆盖这个测试点的过程中,尽可能减少测试步骤让这个测试简短精悍,方便维护。
由于动作式Gherkin是对UI对象的操作,为了方便阅读,加强对互动的UI元素的关注所以一般是 "X is clicked"这种被动的语态。
例子:
When the text field ‘Last name’ is filled with ‘CAO’
反例:
When fill ‘CAO’ into the text field ‘Last name’
和文档式Gherkin一样,使用不同的时态来区分先决条件、行为、期望。
Given步骤用过去时时态,例如:Given user was logged-in.
When步骤用现在时来描述动作, 例如:When button ‘Login’ is clicked.
Then步骤用情态动词描述期望,Then the text ‘Welcome’ should be visible.
动作式Gherkin步骤专注于和UI界面互动,所以尽可能隐藏用户角色信息,一般来说,在Given步骤中给定了一个用户角色即可,而之后,便不在语句中强调用户角色,把重点放在用户界面元素上,这样可以缩短自然语句中的措辞,突出用户界面,这是动作式Gherkin语句最关注的部分。
比如:
例子:
Given the ‘Admin’ was logged-in
When the button ‘Delete user’ is clicked
反例:
Given the ‘Admin’ was logged-in
When the ‘Admin’ clicks the ‘Delete user’ button
UI元素有不同的属性,一些属性是可见的,这样方便用户区别他们,比如一个按钮可以text,class,id来查找到,为了让对象快速被人识别,那么便应该使用人类可见的属性来识别这些自动化UI对象,那么对于这个按钮就应该用按钮的文本来识别,这样便建立了测试语句和软件UI对象视觉上的强有力的联系。
例子:
When the button ‘
反例:
When the button with id ‘
有时候,一些对象没有可视的属性,有时候一些对象是其他对象的分组,或者其他对象的描述,比如一个区域,一个层,这时候,便可以使用id。此时,便需要开发团队给特定的元素添加id来支持自动化测试。
例子:
Then the widget with the id ‘header’ should contain text ’ today’s announcement’
但是要注意的是,这套步骤库的受众是谁,如果是非技术性用户,那么尽可能隐藏掉id这种技术细节。
例子:
Then the element ‘homepage header’ should contain text ’ today’s announcement’
例子1:
Given ‘admin’ was logged-in
And the dashboard page is visible
例子2:
Given the dashboard page is visible
我们可以假设,当dashboard可见的时候,管理员必须要登录,那么文档式Gherkin使用例2便可以聚焦到dashboard相关的信息了。然而,在使用代码实现步骤的时候,将多个动作聚集到一个步骤的定义中,会大大降低一个步骤的可重用性,一个复杂的动作不能像原子动作那样与其他步骤结合。一旦这些步骤链中任何一个地方改变,那么整个步骤就要修改维护。
但另一方面,如果一个场景可以将一些步骤聚合在一起,那么便可以大大提高这个场景的可读性,忽略无关信息,如:Given owner exists. 这步其实聚合一系列创建用户的动作,一句话就表达了整个意思。所以编写方案时,需要在这两种需求之间找到一个平衡。
文档式Gherkin和动作式Gherkin都有它们的适用之处,在写软件需求或者测试时候选择最合适的即可。遵守以上法则,会让定义出来的Gherkin语言符合语言习俗,让英语测试工程师更快速地使用这套步骤快速建立文档和用例,也让自动化框架开发工程师更方便地维护和对接步骤库的使用者。
在提供了基于动作式Gherkin的步骤库后,通过不断地反馈和优化,我们隐藏了对象的ID细节,提供了友好的元素定位方式,以及方便记忆的对象命名库,客户的QA终于可以方便地使用我们的步骤库来书写测试用例了。由于Gherkin步骤本身就是以英语自然语言来书写的,所以它也就自然而然可以用来书写手工测试用例了。一套用例,测试工程师可以看着通俗易懂的Gherkin语言来手动执行用例,Flutter上的类Cucumber自动化框架也可以用自动化执行用例出具报告。这样一来,传统的“手动测试维护一套手工用例,自动化测试再维护一套从手工测试转化成的自动化用例”的工作流不再存在。终于可以大大减少用例的维护和执行开销了。
文/Thoughtworks 曹植野
原文链接:如何定义高质量的Gherkin自然语言步骤库-Thoughtworks洞见