1. Cucumber
Cucumber 是 BDD(Behavior-Driven Development,行为驱动开发)的一个自动化测试工具,使用自然语言来描述测试用例,使得 非研发(QA、PM)也可以理解甚至编写 测试用例。
官方表示:应该将 Cucumber 视为一个【文档编写工具】,而非一个单纯的自动化测试工具
撰写时,应该要以 PM 也能理解 测试用例 为目标去编写 Cucumber
2. Gherkin
Gherkin 是 Cucumber 用来描述 测试用例 的语言,以下为关键字的用意与关联关系。
以 分享概览 为例
Given:新增 帐号[email protected] 、新增 概览1
When:将 概览1 分享给 帐号[email protected]
Then:校验 帐号[email protected] 是否能查看到 概览1
2.1 Scenario 范例
Feature: 授权功能Scenario: 帐号 通过绑定 角色 进行授权 Given 新增角色'分析师',拥有权限'1001, 1002, 1003'And 新增帐号'[email protected]'When 帐号'[email protected]'绑定角色'分析师'Then 鉴权 帐号'[email protected]',有权限'1001'And 鉴权 帐号'[email protected]',有权限'1002'But 鉴权 帐号'[email protected]',没有权限'1005'复制代码
2.2 Background 范例
Feature: 授权功能Background: Given 新增帐号'[email protected]'Scenario: 帐号 通过绑定 角色 进行授权 Given 新增角色'分析师',拥有权限'1001, 1002, 1003'When 帐号'[email protected]'绑定角色'分析师'Then 鉴权 帐号'[email protected]',有权限'1001'And 鉴权 帐号'[email protected]',有权限'1002'But 鉴权 帐号'[email protected]',没有权限'1005'Scenario: 帐号 通过绑定 机构 进行授权 Given 新增机构'北京部门',拥有权限'2001, 2002, 2003'When 帐号'[email protected]'绑定机构'北京部门'Then 鉴权 帐号'[email protected]',有权限'2001'And 鉴权 帐号'[email protected]',有权限'2002'But 鉴权 帐号'[email protected]',没有权限'2005'复制代码
2.3 Scenario Outline 范例
可以在多个 Step 上共用同一个 "简单" 参数,且每一个 Example 都视为一个 Scenario
Feature:授权功能Scenario Outline:帐号通过绑定角色进行授权Given新增角色
3. 基本概念
3.1 文件结构
Gherkin 写在 .feature 文件中
Step 对应的逻辑 写在 .java 文件中
3.2 Step 映射
通过 Gherkin 语法上的描述,找到与 注解 value 值匹配的 Java 方法,将 Gherkin 与 Java 代码关联起来。
3.3 Scenario 独立
当同时执行多个 Scenario 时,执行每个 Scenario 对应的 Java 文件都会被重新创建。
不同的 Scenario 之间,不应该存在数据依赖(MySQL),如果存在依赖,将会使 Scenario 变得脆弱可以在 Backgroud,进行数据清理,来保证测试结果的正确性
二、最佳实践
1. 撰写 Scenairo 原则 - BRIEF
school.cucumber.io/courses/tak…
B:Business Language。
Scenairo 中使用的词语应该使用【业务团队成员】能够理解的词语,否则将无法与业务团队成员互动。
R:Real Data。
Scenairo 中应该使用 具体、真实 的数据(不要用 1、2、3、A、B、C),有助于让场景变得生动,并及早揭示边界条件与基本假设。
I:Intention Revealing。
Scenairo 应该描述试图实现的意图,而不是描述程式将如何实现它的机制。
确保每一行 Step 描述的是 意图 而非 机制。 (比如:创建帐号,就不要写成 "将帐号数据写入 user 表,并在 account_project 表绑定帐号与项目的关联")
E:Essential。
Scenairo 应该只保留必要的 Step,不直接促成结果的场景都应该被删除。
任何不能增加读者对预期行为理解的场景,都不應該出现在文档中。
F:Focus。
多数的 Scenairo 应该只专注于单一职责。
BRIEF
建议将大多数的 Scenairo 限制在五行或更少,这将使它们更易于阅读与推理,并有助于避免 同时测试多个规则 或 增加额外细节。
2. 保证 Scenairo 可读性好处
school.cucumber.io/courses/tak…
随时获得 你做的事情是否正确 的反馈
你的 Feature 可以变成描述你 系统功能 的 线上文档
Scenairo 将会引导你的技术设计
3. 开发流程推荐
school.cucumber.io/courses/tak…
在 Cucumber 中描述你想要实现的 Scenairo,把所有的 Step 串连起来,并运行 Cucumber 使其出现 失败 结果
持续实现 Step 与 API 的具体逻辑,并观察 API 是如何 失败 的,最终使 Scenairo 的结果变为 成功。
当测试通过后,对 API 实现进行 清理 与 优化(重构),使其更具可读性,并再次运行 Cucumber 保证重构后的结果正确。
以上为 测试驱动开发(TDD) 的 生命周期: Red、Green、Clean
如果改坏了系统逻辑,你的测试用例会告诉你。
推荐:在研发进行技术设计前,再多加一个 测试用例评审 的环节,让 PM、QA、RD 一起参与,方便及早发现问题,也能增加技术设计时考量的全面性
三、Cucumber 常用功能
1. 参数化
参数化 可以与 表格化、列表化、对象化 混用
1.1 关键字
类型正则
biginteger"-?\d+" 或者 "\d+"
string"([^"\] (\.[^"\] )*)"
bigdecimal"-?\d*[.,]\d+"
byte"-?\d+" 或者 "\d+"
double"-?\d*[.,]\d+"
short"-?\d+" 或者 "\d+"
float"-?\d*[.,]\d+"
word"\w+"
int"-?\d+" 或者 "\d+"
long"-?\d+" 或者 "\d+"
1.2 说明
Cucumber 支持在 Java 注解 中使用 {关键字} 作为占位符。 在 Step 中直接写上参数,将在 Java 代码中,会把占位符对应的参数作为方法参数传递进去。
字串 类型的 关键字,需要加上 单引号 或 双引号 作为声明
注解中声明占位符的顺序 为 注入方法参数的顺序
1.3 范例
Feature: 授权功能Scenario: 鉴权场景 Given 创建帐号'[email protected]',角色'admin',项目id1复制代码
@Given("创建帐号 {string},角色 {string},项目id {int}")publicvoidtest(Stringusername,Stringrole, Integer projectId) { log.info("start to execute test(), params:[ username = {}, role = {}, projectId = {} ]", username, role, projectId); }复制代码
2. 表格化(DataTable)
2.1 设置 Gherkin 数据
下方的 List - Map、List - List、Map - List 都是共用同一套 Gherkin 代码,也就是说,同一个 Gherkin 代码,Cucumber 可以根据不同的方法参数类型,自动进行转换
Feature: 授权功能Scenario: 鉴权场景 Given 创建帐号 | username | password | role | project_id | | [email protected] | a123456 | admin |1| | [email protected] | b123456 | analyst |2| | [email protected] | c123456 | developer |3|复制代码
2.2 List - Map(常用)
@Given("创建帐号")publicvoid test(List> dataTable) {for(Mapdata: dataTable) { String username =data.get("username"); String password =data.get("password"); String role =data.get("role"); String projectId =data.get("project_id"); log.info("execute test(), fields:[ username = {}, password = {}, role = {}, projectId = {} ]", username, password, role, projectId); } }复制代码
2.3 List - List
@Given("创建帐号")publicvoid test(List> dataTable) {for(Listdata: dataTable) { log.info("execute test(), fields:[ data = {} ]",data); } }复制代码
2.4 Map - List
@Given("创建帐号") publicvoidtest(Map> dataTable) {for(Map.Entry> data : dataTable.entrySet()) {Stringusername = data.getKey(); List infos = data.getValue(); log.info("execute test(), fields:[ username = {}, infos = {} ]", username, infos); } }复制代码
3. 列表化
3.1 直列表
Feature: 授权功能Scenario: 鉴权场景 Given 删除帐号 | [email protected] | | [email protected] | | [email protected] |复制代码
@Given("删除帐号")publicvoidtest(List
3.2 横列表
Feature: 授权功能Scenario: 鉴权场景 Given 删除帐号 | [email protected] | [email protected] | [email protected] |复制代码
@Given("删除帐号") public void test(@TransposeList usernames) {log.info("execute test(), fields:[ usernames = {} ]", usernames); }复制代码
记得加上 @Transpose,告诉 Cucumber 需要进行数据转换
4. 对象化
4.1 撰写 Gherkin 语法
Feature: 授权功能Scenario: 鉴权场景 Given 新增 帐号 | username | role | project | | [email protected] | admin | default | | [email protected] | analyst | production | | [email protected] | developer | default |复制代码
4.2 定义 Java 对象
@DatastaticclassAccount {/**
* 帐号
*/privateStringusername;/**
* 角色
*/privateStringrole;/**
* 项目
*/privateStringproject; }复制代码
4.3 撰写 封装 Java 对象的 方法
/**
* 帐号对象 的 封装方法
*/@DataTableTypepublicAccount defineAccount(Map entry) { Account account = new Account();// 如果有指定数据,则将数据写入 Optional.ofNullable(entry.get("username")).ifPresent(account::setUsername); Optional.ofNullable(entry.get("role")).ifPresent(account::setRole); Optional.ofNullable(entry.get("project")).ifPresent(account::setProject);returnaccount; }复制代码
在封装方法上方,需要加上 @DataTableType 注解
4.4 关联具体 Step 方法
@Given("新增 帐号")publicvoidaddAccount(List
方法参数中,直接指定 对象封装方法 返回的 对象类型,Cucumber 就能直接进行关联
5. 参数化、表格化、列表化 混合使用
DataTable 与 List 必须作为 Java 方法的最后一个参数
5.1 表格化&参数化
Feature: 授权功能Scenario: 鉴权场景 Given 创建帐号,项目'default'| username | password | role | | [email protected] | a123456 | admin | | [email protected] | b123456 | analyst | | [email protected] | c123456 | developer |复制代码
@Given("创建帐号,项目 {string}") publicvoidtest(Stringproject, List> dataTable) { log.info("execute test(), fields:[ project = {} ]", project);for(Map data : dataTable) {Stringusername = data.get("username");Stringpassword = data.get("password");Stringrole = data.get("role"); log.info("execute test(), fields:[ username = {}, password = {}, role = {} ]", username, password, role); } }复制代码
List
5.2 直列表&参数化
Feature: 授权功能Scenario: 鉴权场景 Given 删除帐号,项目'default'| [email protected] | | [email protected] | | [email protected] |复制代码
@Given("删除帐号,项目 {string}")publicvoidtest(StringprojectName, List usernames) { log.info("execute test(), fields:[ projectName = {}, usernames = {} ]", projectName, usernames); }复制代码
List 必须作为最后一个方法参数
5.3 横列表&参数化
Feature: 授权功能Scenario: 鉴权场景 Given 删除帐号,项目'default'| [email protected] | [email protected] | [email protected] |复制代码
@Given("删除帐号,项目 {string}") public void test(String project,@TransposeList usernames) {log.info("execute test(), fields:[ project = {}, usernames = {} ]", project, usernames); }复制代码
5.4 对象化&参数化
Feature: 授权功能Scenario: 鉴权场景 Given 新增 帐号,机构'北京部门'| username | role | project | | [email protected] | admin | default | | [email protected] | analyst | production | | [email protected] | developer | default |复制代码
@Given("新增 帐号,机构 {string}")publicvoidaddAccount(String organization, List
6. 钩子方法(Hook)
6.1 注解
注解执行时机
@BeforeAll在启动 Cucumber 时执行
@Before在所有 Scenario 执行之前执行
@BeforeStep在所有 Step 执行之前执行
@AfterAll在结束 Cucumber 时执行
@After在所有 Scenario 执行之后执行
@AfterStep在所有 Step 执行之后执行
指定注解的 value,可以指定 Hook 的运行范围,不指定则表示在全项目生效。 注解可以定义在项目中的任意位置。
如果同时定义了多个钩子方法,则会依照注解中 order 属性的顺序多次执行。
6.2 全局生效范例
Gherkin 代码
Feature:测试功能 Scenario:测试场景1 Given测试功能1 When测试功能2 Then测试功能3 Scenario:测试场景2 Given测试功能1 When测试功能2 Then测试功能3复制代码
Java 代码
@Slf4jpublicclassTestDefs{@Beforepublicvoidbefore(){ log.info("执行 @Before 方法"); }@BeforeSteppublicvoidbeforeStep(){ log.info("执行 @BeforeStep 方法"); }@Afterpublicvoidafter(){ log.info("执行 @After 方法"); }@AfterSteppublicvoidafterStep(){ log.info("执行 @AfterStep 方法"); }@Given("测试功能1")publicvoidtest1(){ log.info("执行 测试功能1"); }@Given("测试功能2")publicvoidtest2(){ log.info("执行 测试功能2"); }@Given("测试功能3")publicvoidtest3(){ log.info("执行 测试功能3"); } }复制代码
最终结果
6.3 局部生效范例
Gherkin 代码在不同的 Scenario 上,可以增加 自定义注解 作为标记
Feature: 测试功能@MyHookScenario: 测试场景1Given 测试功能1When 测试功能2@YourHookScenario: 测试场景2Given 测试功能1When 测试功能2复制代码
Java 代码Hook 注解的 value 值,可以用来指定,只有当 Scenario 上有该自定义注解时,才会执行 Hook 方法。
如果想将 Hook 方法在多个自定义注解下进行复用,可以通过 , 隔开,例如:@Before("@MyHook,@YourHook")
@Slf4jpublicclassTestDefs{@Before("@MyHook")publicvoidmyHookBefore(){ log.info("执行 @MyHook 的 @Before 方法"); }@BeforeStep("@MyHook")publicvoidmyHookBeforeStep(){ log.info("执行 @MyHook 的 @BeforeStep 方法"); }@Before("@YourHook")publicvoidyourHookBefore(){ log.info("执行 @YourHook 的 @Before 方法"); }@BeforeStep("@YourHook")publicvoidyourHookBeforeStep(){ log.info("执行 @YourHook 的 @BeforeStep 方法"); }@Given("测试功能1")publicvoidtest1(){ log.info("执行 测试功能1"); }@When("测试功能2")publicvoidtest2(){ log.info("执行 测试功能2"); } }复制代码
最终结果
四、Gherkin 模版范例
0. Gherkin 模版撰写规范
所有 模版功能 只运行在 测试项目(project_id = 1) (避免有时要指定 project_id 有时又不需要的麻烦)
使用 Gherkin 参数化 的 字串 功能时,使用 单引号 将字串包裹 (考虑到模版参数可能出现 Json 字串,使用双引号会需要额外进行转译)
模版参数中,不要出现 id(包含 帐号id、角色id、职务id ... 等),应该改用 名称(用户名、角色中文名称、职务名称)去反查 (使用 id 将无可避免的出现要使用 上下文 将多个 Step 进行关联的问题,这将使测试用例变得脆弱,所以宁可用 名称 去反查 id,也不要直接指定 id)
Gherkin 语法表格化的表头字段,使用 下滑线命名法 来命名
模版方法名称定义遵守以下规范 (当一个模版可能同时存在于 Given 与 When 时,以 When 为主)
模版关键字说明范例
Given以 [模块][新增or更新or删除] + 空格 开头[帐号][新增] 新增帐号、[角色][更新] 更新角色
When以 [模块][功能] + 空格 开头[权限][功能] 授权数据资源权限、[权限][功能] 删除数据资源权限数据
Then以 [模块][校验] + 空格 开头[权限][校验] 校验帐号权限
随时注意编写的模版是否尽量符合第 2 大项的最佳实践
1. 帐号
通常为 Given 的最后一步,比如 帐号 要绑定 角色,一定是先创建好 角色,再通过 新增帐号 进行绑定
1.1 新增 帐号
(创建前自动删除原来的帐号、创建时允许指定角色)
属性说明必填默认值
username用户名是无
password密码否AAAaaa111@
user_cname帐号中文名称否与 username 相同
email信箱否与 username 相同
role_cname角色中文名称,默认角色用 role_name 即可否admin
# 最简单的范例Given [帐号][新增] 新增帐号 | username | | [email protected] | | [email protected] | | [email protected] |# 最完整的范例Given [帐号][新增] 新增帐号 | username | password | user_cname | email | role_cname | | [email protected] | AAAaaa111@ | [email protected] | [email protected] | admin | | [email protected] | AAAaaa111@ | [email protected] | [email protected] | analyst | | [email protected] | AAAaaa111@ | [email protected] | [email protected] | developer |复制代码
1.2 更新 帐号 的 角色
Given [帐号][更新] 更新帐号 '[email protected]' 的角色 | 角色A | 角色B | 角色C |复制代码
1.3 更新 帐号 的 职务
Given [帐号][更新] 更新帐号 '[email protected]' 的职务 | 职务A | 职务B | 职务C |复制代码
1.4 删除 帐号
Given [帐号][删除] 删除帐号 | [email protected] | | [email protected] | | [email protected] | | [email protected] |复制代码