一、简介
官方文档:cucumber.io/docs/guides… 官方视频教学(全英文):school.cucumber.io/courses/bdd…
1. Cucumber
Cucumber 是 BDD(Behavior-Driven Development,行为驱动开发)的一个自动化测试工具,使用自然语言来描述测试用例,使得 非研发(QA、PM)也可以理解甚至编写 测试用例。
官方表示:应该将 Cucumber 视为一个【文档编写工具】,而非一个单纯的自动化测试工具
- 撰写时,应该要以 PM 也能理解 测试用例 为目标去编写 Cucumber
2. Gherkin
Gherkin 是 Cucumber 用来描述 测试用例 的语言,以下为关键字的用意与关联关系。
以 分享概览 为例
- Given:新增 帐号 abc@qq.com 、新增 概览1
- When:将 概览1 分享给 帐号 abc@qq.com
- Then:校验 帐号 abc@qq.com 是否能查看到 概览1
2.1 Scenario 范例
Feature: 授权功能
Scenario: 帐号 通过绑定 角色 进行授权
Given 新增角色 '分析师',拥有权限 '1001, 1002, 1003'
And 新增帐号 'abc@qq.com'
When 帐号 'abc@qq.com' 绑定角色 '分析师'
Then 鉴权 帐号 'abc@qq.com',有权限 '1001'
And 鉴权 帐号 'abc@qq.com',有权限 '1002'
But 鉴权 帐号 'abc@qq.com',没有权限 '1005'
2.2 Background 范例
Feature: 授权功能
Background:
Given 新增帐号 'abc@qq.com'
Scenario: 帐号 通过绑定 角色 进行授权
Given 新增角色 '分析师',拥有权限 '1001, 1002, 1003'
When 帐号 'abc@qq.com' 绑定角色 '分析师'
Then 鉴权 帐号 'abc@qq.com',有权限 '1001'
And 鉴权 帐号 'abc@qq.com',有权限 '1002'
But 鉴权 帐号 'abc@qq.com',没有权限 '1005'
Scenario: 帐号 通过绑定 机构 进行授权
Given 新增机构 '北京部门',拥有权限 '2001, 2002, 2003'
When 帐号 'abc@qq.com' 绑定机构 '北京部门'
Then 鉴权 帐号 'abc@qq.com',有权限 '2001'
And 鉴权 帐号 'abc@qq.com',有权限 '2002'
But 鉴权 帐号 'abc@qq.com',没有权限 '2005'
2.3 Scenario Outline 范例
可以在多个 Step 上共用同一个 "简单" 参数,且每一个 Example 都视为一个 Scenario
Feature: 授权功能
Scenario Outline: 帐号 通过绑定 角色 进行授权
Given 新增角色 <role>,拥有权限 <permissions>
When 新增帐号 <account>
Then 帐号 <account> 绑定角色 <role>
And 鉴权 帐号 <account>,有权限 <has_permission>
Examples:
| role | permissions | account | has_permission |
| 分析师 | 1001, 1002, 1003 | abc@qq.com | 1001 |
| 开发者 | 2001, 2002, 2003 | cde@qq.com | 2001 |
| 管理员 | 3001, 3002, 3003 | fgh@qq.com | 3001 |
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 创建帐号 'waiting001@qq.com',角色 'admin',项目id 1
@Given("创建帐号 {string},角色 {string},项目id {int}")
public void test(String username, String role, 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 |
| waiting001@qq.com | a123456 | admin | 1 |
| waiting002@qq.com | b123456 | analyst | 2 |
| waiting003@qq.com | c123456 | developer | 3 |
2.2 List - Map(常用)
@Given("创建帐号")
public void test(List<Map<String, String>> dataTable) {
for (Map<String, String> data : 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("创建帐号")
public void test(List<List<String>> dataTable) {
for (List<String> data : dataTable) {
log.info("execute test(), fields:[ data = {} ]", data);
}
}
2.4 Map - List
@Given("创建帐号")
public void test(Map<String, List<String>> dataTable) {
for (Map.Entry<String, List<String>> data : dataTable.entrySet()) {
String username = data.getKey();
List<String> infos = data.getValue();
log.info("execute test(), fields:[ username = {}, infos = {} ]", username, infos);
}
}
3. 列表化
3.1 直列表
Feature: 授权功能
Scenario: 鉴权场景
Given 删除帐号
| waiting001@qq.com |
| waiting002@qq.com |
| waiting003@qq.com |
@Given("删除帐号")
public void test(List<String> usernames) {
log.info("execute test(), fields:[ usernames = {} ]", usernames);
}
3.2 横列表
Feature: 授权功能
Scenario: 鉴权场景
Given 删除帐号
| waiting001@qq.com | waiting002@qq.com | waiting003@qq.com |
@Given("删除帐号")
public void test(@Transpose List<String> usernames) {
log.info("execute test(), fields:[ usernames = {} ]", usernames);
}
记得加上 @Transpose,告诉 Cucumber 需要进行数据转换
4. 对象化
4.1 撰写 Gherkin 语法
Feature: 授权功能
Scenario: 鉴权场景
Given 新增 帐号
| username | role | project |
| waiting1@qq.com | admin | default |
| waiting2@qq.com | analyst | production |
| waiting3@qq.com | developer | default |
4.2 定义 Java 对象
@Data
static class Account {
/**
* 帐号
*/
private String username;
/**
* 角色
*/
private String role;
/**
* 项目
*/
private String project;
}
4.3 撰写 封装 Java 对象的 方法
/**
* 帐号对象 的 封装方法
*/
@DataTableType
public Account defineAccount(Map<String, String> 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);
return account;
}
在封装方法上方,需要加上 @DataTableType 注解
4.4 关联具体 Step 方法
@Given("新增 帐号")
public void addAccount(List<Account> accountList) {
for (Account account : accountList) {
log.info("execute addAccount(), fields:[ account = {} ]", account);
}
}
方法参数中,直接指定 对象封装方法 返回的 对象类型,Cucumber 就能直接进行关联
5. 参数化、表格化、列表化 混合使用
DataTable 与 List 必须作为 Java 方法的最后一个参数
5.1 表格化&参数化
Feature: 授权功能
Scenario: 鉴权场景
Given 创建帐号,项目 'default'
| username | password | role |
| waiting001@qq.com | a123456 | admin |
| waiting002@qq.com | b123456 | analyst |
| waiting003@qq.com | c123456 | developer |
@Given("创建帐号,项目 {string}")
public void test(String project, List<Map<String, String>> dataTable) {
log.info("execute test(), fields:[ project = {} ]", project);
for (Map<String, String> data : dataTable) {
String username = data.get("username");
String password = data.get("password");
String role = data.get("role");
log.info("execute test(), fields:[ username = {}, password = {}, role = {} ]",
username, password, role);
}
}
List<Map<String, String>> 必须作为最后一个方法参数
5.2 直列表&参数化
Feature: 授权功能
Scenario: 鉴权场景
Given 删除帐号,项目 'default'
| waiting001@qq.com |
| waiting002@qq.com |
| waiting003@qq.com |
@Given("删除帐号,项目 {string}")
public void test(String projectName, List<String> usernames) {
log.info("execute test(), fields:[ projectName = {}, usernames = {} ]", projectName, usernames);
}
List 必须作为最后一个方法参数
5.3 横列表&参数化
Feature: 授权功能
Scenario: 鉴权场景
Given 删除帐号,项目 'default'
| waiting001@qq.com | waiting002@qq.com | waiting003@qq.com |
@Given("删除帐号,项目 {string}")
public void test(String project, @Transpose List<String> usernames) {
log.info("execute test(), fields:[ project = {}, usernames = {} ]", project, usernames);
}
5.4 对象化&参数化
Feature: 授权功能
Scenario: 鉴权场景
Given 新增 帐号,机构 '北京部门'
| username | role | project |
| waiting1@qq.com | admin | default |
| waiting2@qq.com | analyst | production |
| waiting3@qq.com | developer | default |
@Given("新增 帐号,机构 {string}")
public void addAccount(String organization, List<Account> accountList) {
log.info("execute addAccount(), fields:[ organization = {} ]", organization);
for (Account account : accountList) {
log.info("execute addAccount(), fields:[ account = {} ]", account);
}
}
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 代码
@Slf4j
public class TestDefs {
@Before
public void before() {
log.info("执行 @Before 方法");
}
@BeforeStep
public void beforeStep() {
log.info("执行 @BeforeStep 方法");
}
@After
public void after() {
log.info("执行 @After 方法");
}
@AfterStep
public void afterStep() {
log.info("执行 @AfterStep 方法");
}
@Given("测试功能1")
public void test1() {
log.info("执行 测试功能1");
}
@Given("测试功能2")
public void test2() {
log.info("执行 测试功能2");
}
@Given("测试功能3")
public void test3() {
log.info("执行 测试功能3");
}
}
最终结果
6.3 局部生效范例
Gherkin 代码 在不同的 Scenario 上,可以增加 自定义注解 作为标记
Feature: 测试功能
@MyHook
Scenario: 测试场景1
Given 测试功能1
When 测试功能2
@YourHook
Scenario: 测试场景2
Given 测试功能1
When 测试功能2
Java 代码 Hook 注解的 value 值,可以用来指定,只有当 Scenario 上有该自定义注解时,才会执行 Hook 方法。
如果想将 Hook 方法在多个自定义注解下进行复用,可以通过 , 隔开,例如:@Before("@MyHook,@YourHook")
@Slf4j
public class TestDefs {
@Before("@MyHook")
public void myHookBefore() {
log.info("执行 @MyHook 的 @Before 方法");
}
@BeforeStep("@MyHook")
public void myHookBeforeStep() {
log.info("执行 @MyHook 的 @BeforeStep 方法");
}
@Before("@YourHook")
public void yourHookBefore() {
log.info("执行 @YourHook 的 @Before 方法");
}
@BeforeStep("@YourHook")
public void yourHookBeforeStep() {
log.info("执行 @YourHook 的 @BeforeStep 方法");
}
@Given("测试功能1")
public void test1() {
log.info("执行 测试功能1");
}
@When("测试功能2")
public void test2() {
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 相同 |
信箱 | 否 | 与 username 相同 | |
role_cname | 角色中文名称,默认角色用 role_name 即可 | 否 | admin |
# 最简单的范例
Given [帐号][新增] 新增帐号
| username |
| waiting001@qq.com |
| waiting002@qq.com |
| waiting003@qq.com |
# 最完整的范例
Given [帐号][新增] 新增帐号
| username | password | user_cname | email | role_cname |
| waiting001@qq.com | AAAaaa111@ | waiting001@qq.com | waiting001@qq.com | admin |
| waiting002@qq.com | AAAaaa111@ | waiting002@qq.com | waiting002@qq.com | analyst |
| waiting003@qq.com | AAAaaa111@ | waiting003@qq.com | waiting003@qq.com | developer |
1.2 更新 帐号 的 角色
Given [帐号][更新] 更新帐号 'waiting001@qq.com' 的角色
| 角色A | 角色B | 角色C |
1.3 更新 帐号 的 职务
Given [帐号][更新] 更新帐号 'waiting001@qq.com' 的职务
| 职务A | 职务B | 职务C |
1.4 删除 帐号
Given [帐号][删除] 删除帐号
| waiting001@qq.com |
| waiting002@qq.com |
| waiting003@qq.com |
| waiting004@qq.com |